开发

Fetch EXIF升级了

减少查询次数和流量,优化缓存方式,支持Docker部署

2023年4月27日
Fetch EXIF升级了
本文共有963字,预计阅读时间4分钟
最近在学Node,准备写个后台。刚好看完如何CRUD,决定给这个博客利用到的一个小项目做个升级。

之前的问题

如果你随便点开本站的摄影作品,会发现在每张图下面会显示拍摄数据。
Fetch EXIF的功能很简单,接收一个图片的URL,返回图片的拍摄信息。
关于这个功能之前写过文章:利用ChatGPT实现一个Node.js API
但这个基本由ChatGPT完成的API有一些问题,当时我急于完成功能,没有考虑性能。用了这么久,虽然并没有真正遇到性能瓶颈,但一直想优化一下。
之前的API有两个性能问题。
第一个是EXIF和GPS的提取。此前我将这两个查询分成了两个部分,但实际上GPS是EXIF中的一部分。既然已经有了缓存,其实可以将这两部分查询合为一个,分别返回不同的信息即可。
这样做的话,每个页面可以节省一次查询。(因为一个摄影作品只会读一次GPS,EXIF是每张图都有)。
第二个是缓存的方式。当初我采取了ChatGPT的建议,将查询的数据通过node-cache存入内存。照理说就算有上万张图片,那点信息存内存也是够的。但毕竟不是持久方案,程序一旦重启,图片就得重新查询、缓存。如果能将数据存入数据库,应该就能节省很多下载图片的流量。(响应速度其实没区别)

重构

于是重构了这个项目。首先定义了MongoDB的Schema:
1const mongoose = require('mongoose');
2
3const imageSchema = new mongoose.Schema({
4  url: { type: String, required: true, unique: true },
5  exif: {
6    Maker: { type: String, default: 'unknown' },
7    Model: { type: String, default: 'unknown' },
8    ExposureTime: { type: String, default: 'unknown' },
9    FNumber: { type: String, default: 'unknown' },
10    iso: { type: String, default: 'unknown' },
11    FocalLength: { type: String, default: 'unknown' },
12    LensModel: { type: String, default: 'unknown' }
13  },
14  gps: {
15    latitude: { type: Number, required: true, default: 0 },
16    longitude: { type: Number, required: true, default: 0 }
17  },
18  createdAt: { type: Date, expires: '30d', default: Date.now }
19})
20
21module.exports = {
22  Raw: mongoose.model('Raw', imageSchema)
23}
然后再重构获取拍摄信息的函数,将EXIF和GPS信息一次查询并存入:
1const exifr = require("exifr");
2const { Raw } = require("./database");
3
4//将快门数据转换成更容易理解的格式
5function formatShutterTime(shutterTime) {
6  if (!shutterTime) return "0";
7  const time = parseFloat(shutterTime);
8  if (time >= 1) {
9    return time.toFixed(2);
10  }
11  const fraction = Math.round(1 / time);
12  return `1/${fraction}`;
13}
14
15//将axios改为直接使用fetch(),查询到数据后存入数据库
16async function getRaw(url) {
17  try {
18    const response = await fetch(url);
19    const data = new Uint8Array(await response.arrayBuffer());
20    const rawData = await exifr.parse(data);
21    const image = new Raw({
22      url,
23      exif: {
24        Maker: rawData.Make || "unknown",
25        Model: rawData.Model || "unknown",
26        ExposureTime: formatShutterTime(rawData.ExposureTime),
27        FNumber: rawData.FNumber || "unknown",
28        iso: rawData.ISO || "unknown",
29        FocalLength: rawData.FocalLength || "unknown",
30        LensModel: rawData.LensModel || "unknown",
31      },
32      gps: {
33        latitude: rawData.latitude,
34        longitude: rawData.longitude,
35      },
36    });
37    await image.save();
38    return image;
39  } catch (err) {
40    throw err;
41  }
42}
43
44module.exports = getRaw;
最后在主入口函数,稍微修改之前的逻辑,先从数据库中查找,再根据请求返回EXIF或GPS信息:
1const express = require("express");
2const mongoose = require("mongoose");
3const getRaw = require("./lib/getRaw");
4const { Raw } = require("./lib/database");
5
6const app = express();
7const port = 1216;
8
9//连接数据库
10mongoose
11  .connect(process.env.MONGODB_URL || "mongodb://localhost:27017/exif", {
12    useNewUrlParser: true,
13  })
14  .then(() => console.log("Connected to MongoDB"))
15  .catch((err) => console.error("Filed to connect to MongoDB", err));
16
17//以图片url查询数据库,若没有,调用getRaw()
18app.get("/exif", async (req, res) => {
19  const url = req.query.url;
20  const data = await Raw.findOne({ url: url });
21
22  if (!data) {
23    const rawData = await getRaw(url);
24    return res.json(rawData.exif);
25  } else {
26    return res.json(data.exif);
27  }
28});
29
30app.get("/gps", async (req, res) => {
31  const url = req.query.url;
32  const data = await Raw.findOne({ url: url });
33
34  if (!data) {
35    const rawData = await getRaw(url);
36    return res.json(rawData.gps);
37  } else {
38    return res.json(data.gps);
39  }
40});
41
42app.listen(port, () => console.log(`Fetch EXIF is running on port ${port}!`));
这时候运行起一个MongoDB,再npm start,接口就成功运行了。

Docker部署

因为引入数据库,其他人部署会比较麻烦,于是我做了个Docker Image,通过一个docker-compose.yml就能运行起整个接口。
1version: '3'
2services:
3  mongo:
4    image: mongo:5.0.17
5    restart: always
6    ports:
7      - 27017:27017
8    networks:
9      - app-network
10    volumes:
11      - ./data:/data/db
12
13  api:
14    image: darmau/fetch-exif:2.0
15    restart: always
16    ports:
17      - 1216:1216
18    networks:
19      - app-network
20    environment:
21      MONGODB_URL: mongodb://mongo:27017/exif
22    depends_on:
23      - mongo
24
25networks:
26  app-network:
27    driver: bridge

评论