Fetch EXIF升级了

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

开发

-
学习Node.js后,可使用MongoDB和Node.js实现修改摄影作品的EXIF和GPS信息查询功能,优化了性能和可部署性。重构了数据库模型和获取信息函数,通过docker部署简化项目运行。运行整个接口只需使用docker-compose.yml即可。
Fetch EXIF升级了

最近在学Node,准备写个后台。刚好看完如何CRUD,决定给这个博客利用到的一个小项目做个升级。

之前的问题

如果你随便点开本站的摄影作品,会发现在每张图下面会显示拍摄数据。

Fetch EXIF的功能很简单,接收一个图片的URL,返回图片的拍摄信息。

关于这个功能之前写过文章: 利用ChatGPT实现一个Node.js API

但这个基本由ChatGPT完成的API有一些问题,当时我急于完成功能,没有考虑性能。用了这么久,虽然并没有真正遇到性能瓶颈,但一直想优化一下。

之前的API有两个性能问题。

第一个是EXIF和GPS的提取。此前我将这两个查询分成了两个部分,但实际上GPS是EXIF中的一部分。既然已经有了缓存,其实可以将这两部分查询合为一个,分别返回不同的信息即可。

这样做的话,每个页面可以节省一次查询。(因为一个摄影作品只会读一次GPS,EXIF是每张图都有)。

第二个是缓存的方式。当初我采取了ChatGPT的建议,将查询的数据通过 node-cache 存入内存。照理说就算有上万张图片,那点信息存内存也是够的。但毕竟不是持久方案,程序一旦重启,图片就得重新查询、缓存。如果能将数据存入数据库,应该就能节省很多下载图片的流量。(响应速度其实没区别)

重构

于是重构了这个项目。首先定义了MongoDB的Schema:

             const mongoose = require('mongoose');

const imageSchema = new mongoose.Schema({
  url: { type: String, required: true, unique: true },
  exif: {
    Maker: { type: String, default: 'unknown' },
    Model: { type: String, default: 'unknown' },
    ExposureTime: { type: String, default: 'unknown' },
    FNumber: { type: String, default: 'unknown' },
    iso: { type: String, default: 'unknown' },
    FocalLength: { type: String, default: 'unknown' },
    LensModel: { type: String, default: 'unknown' }
  },
  gps: {
    latitude: { type: Number, required: true, default: 0 },
    longitude: { type: Number, required: true, default: 0 }
  },
  createdAt: { type: Date, expires: '30d', default: Date.now }
})

module.exports = {
  Raw: mongoose.model('Raw', imageSchema)
}
           

然后再重构获取拍摄信息的函数,将EXIF和GPS信息一次查询并存入:

             const exifr = require("exifr");
const { Raw } = require("./database");

//将快门数据转换成更容易理解的格式
function formatShutterTime(shutterTime) {
  if (!shutterTime) return "0";
  const time = parseFloat(shutterTime);
  if (time >= 1) {
    return time.toFixed(2);
  }
  const fraction = Math.round(1 / time);
  return `1/${fraction}`;
}

//将axios改为直接使用fetch(),查询到数据后存入数据库
async function getRaw(url) {
  try {
    const response = await fetch(url);
    const data = new Uint8Array(await response.arrayBuffer());
    const rawData = await exifr.parse(data);
    const image = new Raw({
      url,
      exif: {
        Maker: rawData.Make || "unknown",
        Model: rawData.Model || "unknown",
        ExposureTime: formatShutterTime(rawData.ExposureTime),
        FNumber: rawData.FNumber || "unknown",
        iso: rawData.ISO || "unknown",
        FocalLength: rawData.FocalLength || "unknown",
        LensModel: rawData.LensModel || "unknown",
      },
      gps: {
        latitude: rawData.latitude,
        longitude: rawData.longitude,
      },
    });
    await image.save();
    return image;
  } catch (err) {
    throw err;
  }
}

module.exports = getRaw;
           

最后在主入口函数,稍微修改之前的逻辑,先从数据库中查找,再根据请求返回EXIF或GPS信息:

             const express = require("express");
const mongoose = require("mongoose");
const getRaw = require("./lib/getRaw");
const { Raw } = require("./lib/database");

const app = express();
const port = 1216;

//连接数据库
mongoose
  .connect(process.env.MONGODB_URL || "mongodb://localhost:27017/exif", {
    useNewUrlParser: true,
  })
  .then(() => console.log("Connected to MongoDB"))
  .catch((err) => console.error("Filed to connect to MongoDB", err));

//以图片url查询数据库,若没有,调用getRaw()
app.get("/exif", async (req, res) => {
  const url = req.query.url;
  const data = await Raw.findOne({ url: url });

  if (!data) {
    const rawData = await getRaw(url);
    return res.json(rawData.exif);
  } else {
    return res.json(data.exif);
  }
});

app.get("/gps", async (req, res) => {
  const url = req.query.url;
  const data = await Raw.findOne({ url: url });

  if (!data) {
    const rawData = await getRaw(url);
    return res.json(rawData.gps);
  } else {
    return res.json(data.gps);
  }
});

app.listen(port, () => console.log(`Fetch EXIF is running on port ${port}!`));
           

这时候运行起一个MongoDB,再 npm start ,接口就成功运行了。

Docker部署

因为引入数据库,其他人部署会比较麻烦,于是我做了个Docker Image,通过一个 docker-compose.yml 就能运行起整个接口。

             version: '3'
services:
  mongo:
    image: mongo:5.0.17
    restart: always
    ports:
      - 27017:27017
    networks:
      - app-network
    volumes:
      - ./data:/data/db

  api:
    image: darmau/fetch-exif:2.0
    restart: always
    ports:
      - 1216:1216
    networks:
      - app-network
    environment:
      MONGODB_URL: mongodb://mongo:27017/exif
    depends_on:
      - mongo

networks:
  app-network:
    driver: bridge