使用 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 的文档。
如果该帖子对您有帮助,请点赞并为该仓库加注星标。
