什么是GridFs ?
GridFs 是用于存储音频、视频或图像等大型文件的 mongodb 规范……它最适用于存储超过 mongodb 文档大小限制(16MB)的文件。 此外,无论文件大小如何,当您想要存储访问文件,但不想将整个文件加载到内存中时,它也很有用。
GridFs 如何工作 ?
当您将文件上传到 GridFs 存储桶时,GridFs 不会将文件存储在单个文档中,而是将其分成称为块的小块,并将每个块存储作为单独的文档,每个块的最大大小为 255kB,最后一个块除外,它可以尽可能大。
为了存储块和文件的元数据(文件名、大小、文件上传时间等),GridFS 默认使用两个集合,fs.files 和 fs.chunks。 每个块都由其唯一的 _id (ObjectId) 字段标识。 fs.files 用作父文档。 fs.chunks 文档中的 files_id 字段建立了 fs.files 和 fs.chuncks 集合文档之间的一对多关系。
如何将 GridFs 与 Node.js 和 mongodb 一起使用?
先决条件
- 安装了NodeJS LTS
- 了解如何连接 MongoDB Atlas
- 代码编辑器
本教程介绍了什么内容?
- 创建GridFS Bucket
- 上传文件
- 下载文件
- 重命名文件
- 删除文件
安装
首先,您需要一个node.js项目。 让我们从初始化一个新文件夹开始吧。
mkdir gridfs-tutorial; cd gridfs-tutorial; npm init -y
这将创建一个具有标准默认值的 package.json 文件。 我们的项目文件夹已准备好了,但让我们先安装一些依赖项
npm i express morgan body-parser mongoose multer-gridfs-storage multer dotenv
Express:Express js 是一个 node.js 路由和中间件 Web 框架,它为 Web 和移动应用程序提供了强大的功能支持。
Morgan:Morgan 是一个用于 node.js 的 HTTP 请求记录中间件
Body-parser:body-parser 是一个node.js 中间件,在处理之前解析中间件中的传入请求主体
Mongoose:Mongoose 是用于 MongoDB 和 Node.js 的对象数据建模 (ODM) 库。 它提供模式验证,管理数据之间的关系,并用于在代码中的对象和 MongoDB 中这些对象的表示之间进行转换。
Multer:Multer 是一个node.js 处理 multipart/form-data 的中间件,主要用于上传文件
Multer-gridfs-storage:Multer-gridfs-storage 是 GridFS 的 Multer 引擎,允许将上传的文件直接存储到 MongoDb
Dotenv:Dotenv 是一个 npm 包,它自动将环境变量从 .env 文件加载到 process.env 对象中。
开发依赖
让我们安装 Nodemon 作为开发依赖,以便在文件更改后自动重启服务器
npm install --save-dev nodemon
设置Express服务器
创建一个名为 index.js 的文件,这将是我们的Express服务器入口文件,以下是代码:
const express = require("express");
const bodyParser = require("body-parser");
const logger = require("morgan");
const dotenv = require("dotenv");
dotenv.config();
const app = express();
// Connect to database
// Connect to MongoDB GridFS bucket using mongoose
// Middleware for parsing request body and logging requests
app.use(bodyParser.json());
app.use(logger("dev"));
// Routes for API endpoints
// Server listening on port 3000 for incoming requests
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
配置服务器启动
在 package.json 文件中,将scripts部分更改为
"scripts": {
"dev": "nodemon index.js"
}
这将允许服务器在文件更改后自动重新启动。
运行服务器
使用以下命令启动服务器 :
npm run dev
您应该在终端中看到以下消息:
Server listening on port 3000
将Node.js/Express.js 项目连接到mongodb 数据库
— 在项目的根目录下创建一个 .env 文件,然后添加以下变量
MONGO_DB: 您的 mongodb 数据库链接。
MONGO_USER: 此变量采用您在 mongodb atlas 上创建的用户名
MONGO_USER_PWD: 此变量的值是您使用上面的用户名创建的密码
— 在项目的根目录创建数据库文件夹,然后在该文件夹中创建 config.js 文件,代码内容是:
//config.js
const mongoose = require("mongoose");
const connectDB = async () => {
try {
await mongoose.connect(`mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_USER_PWD}@cluster0.vlhig1a.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`,);
console.log("MongoDB connected");
} catch (err) {
console.error(err.message);
process.exit(1);
}
};
module.exports = connectDB;
— 更新index.js文件
const express = require("express");
const bodyParser = require("body-parser");
const logger = require("morgan");
const dotenv = require("dotenv");
const connectDB = require("./database/config");
dotenv.config();
const app = express();
// Connect to database
connectDB();
// Connect to MongoDB GridFS bucket using mongoose
// Middleware for parsing request body and logging requests
app.use(bodyParser.json());
app.use(logger("dev"));
// Routes for API endpoints
// Server listening on port 3000 for incoming requests
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
— 服务器重新启动时,您应该会看到以下消息:
Server listening on port 3000
MongoDB connected
设置GridFs bucket
— 让我们创建一个 GridFs Bucket 的实例
导入 mongoose 更新您的 index.js 文件,并在 connectDB() 调用之后添加以下代码:
let bucket;
(() => {
mongoose.connection.on("connected", () => {
bucket = new mongoose.mongo.GridFSBucket(mongoose.connection.db, {
bucketName: "filesBucket",
});
});
})();
在这里,我们正在创建一个我们的存储bucket实例,以便对文件进行一些操作(获取、更新、删除、重命名……),如果不存在同名的bucket,将创建一个具有该名称的bucket。
重新启动服务器,您应该会看到以下消息:
Server listening on port 3000
Bucket is ready to use
MongoDB connected
— 让我们管理文件存储
在项目的根目录下创建一个 utils 文件夹,然后在其中创建一个 upload.js 文件。
//upload.js
const multer = require("multer");
const { GridFsStorage } = require("multer-gridfs-storage");
// Create storage engine
export function upload() {
const mongodbUrl= `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_USER_PWD}@cluster0.vlhig1a.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`;
const storage = new GridFsStorage({
url: mongodbUrl,
file: (req, file) => {
return new Promise((resolve, _reject) => {
const fileInfo = {
filename: file.originalname,
bucketName: "filesBucket",
};
resolve(fileInfo);
});
},
});
return multer({ storage });
}
module.exports = { upload };
将 mongodbUrl 替换为您自己的 mongodb 连接语句,就像您之前在上面所做的那样。
以下是上传文件的工作流程:
Express 是将文件上传到 MongoDB 的框架
Bodyparser 从 HTML 表单中检索基本内容
Multer 处理文件上传
Multer-gridfs storage 将 GridFS 与 multer 集成,用于在 MongoDB 中存储大文件。
在这里,作为 new GridFsStorage(...) 的参数,我们有一个具有两个属性的对象:
url : 它指的是我们的 mongodb Atlas 集群的 url
file : file属性的值是一个控制文件在数据库中存储的函数,它按文件(例如,在多个文件上传的情况下)使用参数 req 和 file 按此顺序调用。 它返回一个对象或解析为具有以下属性的对象的承诺。
filename:文件所需的文件名(默认:16 字节十六进制名称,不带扩展名)
id:用作标识符的 ObjectID(默认值:自动生成)
metadata:文件的元数据(默认值:null)
chunkSize:文件块的大小,以字节为单位(默认值:261120)
bucketName:存储文件的GridFs集合(默认:fs)
contentType:文件的内容类型(默认:从请求中推断)
aliases:可选的字符串数组,存储在文件文档的别名字段中(默认值:null)
disableMD5:如果为 true,则禁用向文件数据添加 md5 字段(默认值:false,仅在 MongoDb >= 3.1 上可用)
上传单个文件
//Routes for API endpoints 注释后添加如下代码:
const { upload } = require("./utils/upload");
//...
// Upload a single file
app.post("/upload/file", upload().single("file"), async (req, res) => {
try {
res.status(201).json({ text: "File uploaded successfully !" });
} catch (error) {
console.log(error);
res.status(400).json({
error: { text: "Unable to upload the file", error },
});
}
});
在这里你可能会说那是什么,但别担心,让我解释一下:
首先Express 是一个中间件框架,这意味着它将首先执行我们的 upload() 函数,然后再执行数组函数。
— 让我们详细看看 upload().single("file")
upload() 返回一个 Multer 实例,该实例提供多种方法来生成处理以 multipart/form-dataformat 上传的文件的中间件。 single(...) 方法是其中一种方法,它返回处理与给定表单字段关联的单个文件的中间件。 它的参数“file”必须与处理文件上传的客户端表单输入的名称相同。
文件上传后,我们的第二个函数(数组函数)将被调用并响应客户端请求。
上传多个文件
使用以下代码更新 index.js 文件:
// Upload multiple files
app.post("/upload/files", upload().array("files"), async (req, res) => {
try {
res.status(201).json({ text: "Files uploaded successfully !" });
} catch (error) {
console.log(error);
res.status(400).json({
error: { text: `Unable to upload files`, error },
});
}
});
Multer 的 array(...) 方法返回处理共享相同字段名称的多个文件的中间件。
下载单个文件
要从 GridFs 存储桶中检索文件,可以使用 openDownloadStream。
— 它需要两个参数:
id: 您要下载的文件的 ObjectId
options: 描述如何检索数据的对象,它有两个属性
start (Number): 可选的基于 0 的偏移量(以字节为单位)以开始流式传输
end (Number): 可选的基于 0 的字节偏移量以在之前停止流式传输
— 它将文件作为可读流返回,您可以将其通过管道传递给客户端请求响应。
以下是如何通过文件 ID 下载文件:
// Download a file by id
app.get("/download/files/:fileId", async (req, res) => {
try {
const { fileId } = req.params;
// Check if file exists
const file = await bucket.find({ _id: new mongoose.Types.ObjectId(fileId) }).toArray();
if (file.length === 0) {
return res.status(404).json({ error: { text: "File not found" } });
}
// set the headers
res.set("Content-Type", file[0].contentType);
res.set("Content-Disposition", `attachment; filename=${file[0].filename}`);
// create a stream to read from the bucket
const downloadStream = bucket.openDownloadStream(new mongoose.Types.ObjectId(fileId));
// pipe the stream to the response
downloadStream.pipe(res);
} catch (error) {
console.log(error);
res.status(400).json({error: { text: `Unable to download file`, error }});
}
});
我们在这里所做的很简单:
- 首先,我们从请求参数中获取文件 ID“field”,然后我们搜索其 _id 属性等于 fileId 的文件
- 由于 bucket 的 find(…) 方法返回一个数组,检查它是否至少有一个项目,否则我们告诉客户端我们没有找到任何具有此 id 的文件
- 如果我们找到一个文件,我们将一些参数设置为响应标头,例如文件类型
- 然后我们将文件下载为可读流
- 并将该流通过管道传递给响应
下载多个文件
您几乎不会看到教程向您解释如何检索多个文件并将其发送到客户端。 在这里,我将展示实现这一目标的两种方法。 让我们从第一种方法开始:
— 使用archiverjs
Archiverjs 是一个用于生成压缩包的 nodejs 流接口。 您可以像这样安装它:
npm install archiver --save
我们将在这里使用 archiverjs 来收集我们的数据,然后将它们压缩为 zip 文件,然后再将它们发送给客户端。 这是它的工作原理:
//在 _**index.js**_ 文件的顶部导入 _**archiver**_:
const archiver = require("archiver")
//...
app.get("/download/files", async (req, res) => {
try {
const files = await bucket.find().toArray();
if (files.length === 0) {
return res.status(404).json({ error: { text: "No files found" } });
}
res.set("Content-Type", "application/zip");
res.set("Content-Disposition", `attachment; filename=files.zip`);
res.set("Access-Control-Allow-Origin", "*");
const archive = archiver("zip", {
zlib: { level: 9 },
});
archive.pipe(res);
files.forEach((file) => {
const downloadStream = bucket.openDownloadStream(
new mongoose.Types.ObjectId(file._id)
);
archive.append(downloadStream, { name: file.filename });
});
archive.finalize();
} catch (error) {
console.log(error);
res.status(400).json({
error: { text: `Unable to download files`, error },
});
}
});
我们在这里做的很简单:首先,我们从 filesBucket.file.collection 中检索所有文件元数据,然后使用该文件元数据数组,通过 foreach 循环,将每个文件下载为可读流,并将该流附加到存档数据, 传送到响应对象。 当所有文件都收集到存档时,我们最终确定存档器实例并防止进一步附加到存档结构。
现在您可能想知道如何从客户端读取此 zip 数据。 为此,您可以使用 jszip 包。
— 将每个文件数据转换为 base64 字符串
在这里我们不需要安装任何他们的库。 我们将使用 nodejs 内置模块。
// 在 _**index.js**_ 文件顶部的 nodejs bultin 流模块导入 **_Transform_** 类:
const { Transform } = require("stream");
//...
app.get("/download/files2", async (_req, res) => {
try {
const cursor = bucket.find();
const files = await cursor.toArray();
const filesData = await Promise.all(
files.map((file) => {
return new Promise((resolve, _reject) => {
bucket.openDownloadStream(file._id).pipe(
(() => {
const chunks = [];
return new Transform({
// transform method will
transform(chunk, encoding, done) {
chunks.push(chunk);
done();
},
flush(done) {
const fbuf = Buffer.concat(chunks);
const fileBase64String = fbuf.toString("base64");
resolve(fileBase64String);
done();
// use the following instead if you want to return also the file metadata (like its name and other information)
/*const fileData = {
...file, // file metadata
fileBase64String: fbuf.toString("base64"),
};
resolve(fileData);
done();*/
},
});
})()
);
});
})
);
res.status(200).json(filesData);
} catch (error) {
console.log(error);
res.status(400).json({
error: { text: `Unable to retrieve files`, error },
});
}
});
我们的工作流程与以前的方法几乎相同。 我们首先从 filesBucket.file.collection 中检索所有文件元数据,然后从该文件元数据数组中,我们使用 map 函数创建一个新数组,方法是将每个文件作为流下载,然后将流传输到转换流的转换类 转化为base64字符串,通过Promise.resolve()将转换后的数据返回给新的数组,并将数据发送给客户端。
现在您知道了如何以不同的方式从 GridFs 存储桶中检索单个文件和多个文件。 让我们看看如何重命名和删除文件。
重命名文件
要重命名文件,我们可以使用 GridFs 存储桶的 rename(...) 方法。 该方法采用三个参数:
id (ObjectId): 要重命名的文件的id
filename(String):文件的新名称
callback(GridFSBucket~errorCallback): 一个可选的回调函数,将在文件重命名被尝试(成功与否)后执行
// Rename a file
app.put("/rename/file/:fileId", async (req, res) => {
try {
const { fileId } = req.params;
const { filename } = req.body;
await bucket.rename(new mongoose.Types.ObjectId(fileId), filename);
res.status(200).json({ text: "File renamed successfully !" });
} catch (error) {
console.log(error);
res.status(400).json({
error: { text: `Unable to rename file`, error },
});
}
});
要测试文件重命名功能,请从您的 mongodb 数据库中复制文件的 ID 并将其粘贴
删除文件
要从bucket 中删除文件,我们可以使用 GridFs bucket的 delete(..) 方法。 该方法采用二个参数:
id(ObjectId): 文件ID
callbck(GridFSBucket~errorCallback): 尝试删除文件后执行的可选回调函数
您可以使用Postman测试如何删除具有给定 Id 的文件
下一步是什么 ?
只有实践才能帮助您提高知识和技能。 将本教程中获得的知识应用到具体项目中。