如何使用 GridFS 、 Node.js、Mongodb和Multer 管理文件存储?
wptr33 2025-05-02 13:52 3 浏览
什么是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 的文件
下一步是什么 ?
只有实践才能帮助您提高知识和技能。 将本教程中获得的知识应用到具体项目中。
相关推荐
- Spring和SpringBoot到底有什么区别
-
一提到Spring和SpringBoot的区别,大部分人第一反应就是SpringBoot是Spring的框架,那具体的区别在哪里呢?为什么现在开发都用SpringBoot呢?...
- Spring Boot3.0升级,踩坑之旅,附解决方案
-
本文基于newbeemall项目升级SpringBoot3.0踩坑总结而来,附带更新说明:...
- Java常用框架,你用过几款?(java使用的框架)
-
作为头牌编程语言,Java的火爆程度已经毋庸置疑,Java框架在Java开发中有着不可忽视的重要地位。今天就给大家具体介绍一下Java常用框架,希望对正在学习Java的小伙伴有所帮助。框架、设计模式框...
- 2021年超详细的java学习路线总结—纯干货分享
-
本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础...
- Nginx+SpringBoot实现负载均衡(nginx负载均衡的实现)
-
作者:虚无境出处:http://www.cnblogs.com/xuwujing前言在上一篇中介绍了Nginx的安装,本篇文章主要介绍的是Nginx如何实现负载均衡。负载均衡介绍介绍在介绍Nginx的...
- Spring Boot 运行原理(5分钟速解)
-
SpringBoot...
- SpringBoot+LayUI后台管理系统开发脚手架
-
源码获取方式:关注,转发之后私信回复【源码】即可免费获取到!项目简介本项目本着避免重复造轮子的原则,建立一套快速开发JavaWEB项目(springboot-mini),能满足大部分后台管理系统基础开...
- java轻松玩转Excel之EasyExcel(java做excel)
-
项目地址:https://github.com/PiKeZhao/excel-model.git如果您对该项目有什么问题加群咨询哦978219630(各类电子书籍,学习视频等)大家常用Apache...
- 开源一套简单通用的后台管理系统(开源系统靠什么赚钱)
-
前言 前段时间我们写一个简单的后台模板SpringBoot系列——Security+Layui实现一套权限管理后台模板<...
- VUE简介(vue简介和特点)
-
一.前后端分离既然我们在开发中使用前后端分离模式,也就是前端拿到后端的数据时怎么处理,怎么输出都有前端自己来实现,这样就需要写大量的js代码,而为了简化js的代码,就衍生出了很多的框架,比如jquer...
- 聊聊如何对eureka管理界面进行定制化改造
-
前言在nacos还未面世之前,eureka基本上就是springcloud全家桶体系注册中心的首选,随着nacos的横空出世,越来越多基于springcloud的微服务项目采用nacos作为注册中心,...
- newbee-mall开源免费java商城系统
-
简介newbee-mall项目(新蜂商城)是一套电商系统,包括newbee-mall商城系统及newbee-mall-admin商城后台管理系统,基于SpringBoot2.X及相关...
- 入职阿里巴巴,成为年薪百万阿里P7高级架构师需要必备哪些技术栈
-
大家都知道,阿里P7高级技术专家,基本上是一线技术人能达到的最高职级,也是很多程序员追求的目标。达到年入百万的P7Java高级架构师级别,不仅要具备优秀的编程能力和系统设计能力,在技术视野和业务洞...
- 学完SSM框架就可以成为Java程序员了?要找到工作还需要这些技术
-
Java语言是学习人数最多的语言,没错,应用领域的优势和就业薪资的吸引是不少人关注Java语言的理由。但其实Java也是一门“宽进严出”的编程语言,想成为Java高手并不容易。那么学到什么程度才能出师...
- SpringCloud系列——SSO 单点登录
-
前言 作为分布式项目,单点登录是必不可少的,文本基于之前的的博客(猛戳:SpringCloud系列——Zuul动态路由,SpringBoot系列——Redis)记录Zuul配合Redis实现一...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mysql max (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)