使用 NodeJS 通过 GridFS 和 Multer 将文件上传到 MongoDB
GenAI LIVE! | 2025年6月4日
您好,在本教程中,我们将学习如何使用GridFS规范将文件直接上传到MongoDB 。
如果您认为 TLDR;只需检查此处的完成代码。
官方文档解释了何时使用此规范上传文件。总结如下:
- 
  如果您的文件系统限制目录中的文件数量,则可以使用 GridFS 来存储所需数量的文件。 
- 
  当您想要访问大文件部分的信息而不必将整个文件加载到内存中时,您可以使用 GridFS 来调用文件的各部分,而无需将整个文件读入内存。 
- 
  当您希望文件和元数据在多个系统和设施之间自动同步和部署时,可以使用 GridFS。使用地理分布的副本集时,MongoDB 可以自动将文件及其元数据分发到多个 mongod 实例和设施。 
由于 GridFS 是以块的形式存储文件的。以下是创建的集合:
- chunks存储二进制块。
- 文件存储文件的元数据。
先决条件
- NodeJS 长期支持版本
- 本地机器上安装的 MongoDB
- 代码编辑器
设置本地 NodeJS 服务器
转到命令行,然后输入
npm init -y
这将生成一个具有默认值的 package.json 文件。
然后安装该项目所需的所有依赖项
npm install express mongoose ejs multer multer-gridfs-storage 
在项目根目录中创建一个名为app.js的文件。需要创建服务器所需的包。
const express = require("express");
const app = express();
app.use(express.json());
app.set("view engine", "ejs");
const port = 5001;
app.listen(port, () => {
  console.log("server started on " + port);
});
我们最好创建脚本从命令行运行 Web 应用程序,转到 package.json 文件并在 scripts 键上添加以下内容:
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  }
然后运行npm start,服务器应该在端口 5001 上启动。您应该在命令行上看到一个日志,指出服务器在 5001 上启动。
连接数据库、初始化 GridFsStorage 并创建存储
需要所有必要的软件包
const crypto = require("crypto");
const path = require("path");
const mongoose = require("mongoose");
const multer = require("multer");
const GridFsStorage = require("multer-gridfs-storage");
Mongoose 是 MongoDB 的一个 ORM,本教程将使用它。Multer 是一个 NodeJS 中间件,用于简化文件上传。GridFsStorage 是 Multer 的 GridFS 存储引擎,用于将上传的文件直接存储到 MongoDB。Crypto 和 Path 将用于为上传的文件创建唯一的名称。
// DB
const mongoURI = "mongodb://localhost:27017/node-file-upl";
// connection
const conn = mongoose.createConnection(mongoURI, {
  useNewUrlParser: true,
  useUnifiedTopology: true
});
现在,初始化 GridFsStorage
// init gfs
let gfs;
conn.once("open", () => {
  // init stream
  gfs = new mongoose.mongo.GridFSBucket(conn.db, {
    bucketName: "uploads"
  });
});
这里我们使用 mongoose 使用的原生 nodejs-mongodb-drive 并创建一个 GridFSBucket,我们将 db 传递给 bucket,您可以看到我们给出了一个 bucket 名称,这个 bucket 名称将用作集合的名称。
// Storage
const storage = new GridFsStorage({
  url: mongoURI,
  file: (req, file) => {
    return new Promise((resolve, reject) => {
      crypto.randomBytes(16, (err, buf) => {
        if (err) {
          return reject(err);
        }
        const filename = buf.toString("hex") + path.extname(file.originalname);
        const fileInfo = {
          filename: filename,
          bucketName: "uploads"
        };
        resolve(fileInfo);
      });
    });
  }
});
const upload = multer({
  storage
});
现在我们根据 Multer GridFS 初始化存储,并使用加密库中的 randomBytes 方法创建随机字节。
这里我们使用 Promise 构造函数创建一个 Promise,然后将其解析为 fileInfo 对象。此步骤是可选的,因为您只需传递一个 URL 键,存储桶即可正常工作,并且不会更改文件名。例如,您可以像下面这样使用:
const storage = new GridFsStorage({ url : mongoURI})
接下来让我们用模板引擎设置我们的前端并配置 express 来呈现模板。
创建视图
在文件夹根目录下创建一个名为“views”的新文件夹,并在其中创建一个名为“index.ejs”的文件。我们将在这里存储前端视图。我不会费力地讲解 HTML 代码,直接贴出代码就好了。我使用 Bootstrap 进行快速原型设计。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    <title>Mongo File Upload</title>
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-6 m-auto">
                <h1 class="my-4">Lets upload some stuff</h1>
                <form action="/upload" method="post" enctype="multipart/form-data">
                    <div class="custom-file mb-3">
                        <input type="file" class="custom-file-input" name="file" id="file1" onchange="readSingleFile(this.files)">
                        <label class="custom-file-label" for="file1" id="file-label">Choose file</label>
                    </div>
                    <input type="submit" value="Submit" class="btn btn-primary btn-block">
                </form>
            </div>
        </div>
    </div>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
    <script>
        function readSingleFile(e) {
            const name = e[0].name;
            document.getElementById("file-label").textContent = name;
        }
    </script>
</body>
</html>
设置 Express 应用来渲染视图。将视图引擎中间件设置为 ejs
....
app.use(express.json());
app.set("view engine", "ejs");
....
app.get("/", (req, res) => {
 res.render("index")
})
然后再次启动服务器,转到浏览器并打开http://localhost:5001,您应该看到一个使用我们刚刚创建的视图呈现的页面。
创建请求来处理表单提交并上传文件
app.post("/upload", upload.single("file"), (req, res) => {
  res.redirect("/");
});
因为我们已经完成了大部分繁重的工作,创建存储桶时,multer 会处理剩下的事情。我们只需要传递中间件,然后重定向到相同的 URL 即可。
棘手的部分是下载或在本例中从 GridFS 存储桶中流式传输数据并呈现图像,为此我们将创建一个用于显示图像的路由,该路由将文件的名称作为参数或作为路由参数传递。
app.get("/image/:filename", (req, res) => {
  // console.log('id', req.params.id)
  const file = gfs
    .find({
      filename: req.params.filename
    })
    .toArray((err, files) => {
      if (!files || files.length === 0) {
        return res.status(404).json({
          err: "no files exist"
        });
      }
      gfs.openDownloadStreamByName(req.params.filename).pipe(res);
    });
});
在 gridfs bucket 上,我们可以访问许多方法,其中之一就是 find,它与 MongoDB 中的常规 find 非常相似,接受文件名作为第一个参数,然后我们将结果转换为数组并检查是否有具有此文件名的文件,如果有,我们使用 gridfs bucket 上的另一个名为openDownloadStreamByName的方法,它再次获取文件名,然后我们使用管道将响应返回给客户端。
到目前为止,我们可以通过上述路由获取图像,但无法在我们的视图上呈现它,所以让我们在呈现 index.ejs 页面的路由内创建一个方法。
....
app.get("/", (req, res) => {
  if(!gfs) {
    console.log("some error occured, check connection to db");
    res.send("some error occured, check connection to db");
    process.exit(0);
  }
  gfs.find().toArray((err, files) => {
    // check if files
    if (!files || files.length === 0) {
      return res.render("index", {
        files: false
      });
    } else {
      const f = files
        .map(file => {
          if (
            file.contentType === "image/png" ||
            file.contentType === "image/jpeg"
          ) {
            file.isImage = true;
          } else {
            file.isImage = false;
          }
          return file;
        })
        .sort((a, b) => {
          return (
            new Date(b["uploadDate"]).getTime() -
            new Date(a["uploadDate"]).getTime()
          );
        });
      return res.render("index", {
        files: f
      });
    }
  });
});
....
在这里您可以看到很多可选代码,例如数组的排序,您可以跳过这些代码。
现在,在模板上,我们循环遍历发送的文件,然后在表单下方显示图片。我们只会渲染 jpg 或 png 类型的文件,该校验可以通过正则表达式进行升级,具体取决于个人偏好。
        <hr>
                <% if(files) { %>
                <% files.forEach(function(file) {%>
                <div class="card mb-3">
                    <div class="card-header">
                        <div class="card-title">
                                <%= file.filename %>
                        </div>
                    </div>
                    <div class="card-body">
                        <% if (file.isImage) { %>
                    <img src="image/<%= file.filename %>" width="250" alt="" class="img-responsive">
                        <%} else { %>
                        <p><% file.filename %></p>
                        <% } %>
                    </div>
                    <div class="card-footer">
                        <form action="/files/del/<%= file._id %>" method="post">
                            <button type="submit" class="btn btn-danger">Remove</button>
                        </form>
                    </div>
                </div>
                <%}) %>
                <% } else { %>
                <p>No files to show</p>
                <% } %>
您可以看到上面的代码中有一个删除按钮,因此让我们创建一个删除路由来从数据库中删除该文件。
// files/del/:id
// Delete chunks from the db
app.post("/files/del/:id", (req, res) => {
  gfs.delete(new mongoose.Types.ObjectId(req.params.id), (err, data) => {
    if (err) return res.status(404).json({ err: err.message });
    res.redirect("/");
  });
});
这里我们获取的 id 是一个字符串,因此需要将其转换为 mongodb objectid,然后只有 bucket 方法才能删除对应 id 的文件。为了简单起见,这里我没有使用 delete HTTP 方法,您可以随意使用它,如果愿意的话,post 请求在这里就很好用。
结论
我们可以看到,MongoDB 提供了一个很好的解决方案来在数据库中存储文件,并且在创建存储空间较少的 WebApp 时非常方便,但请记住,您只能存储最大 16mb 的文档。
如果该帖子对您有帮助,请点赞并为该仓库加注星标。
 
 后端开发教程 - Java、Spring Boot 实战 - msg200.com
            后端开发教程 - Java、Spring Boot 实战 - msg200.com
          
