不能直接删GridFS文件,因delete()会永久删除fs.files和fs.chunks中所有文档且不可恢复;必须应用层实现软删除:添加deletedAt字段、用updateOne更新状态、查询时显式过滤$eq: null。

为什么不能直接删GridFS的文件
GridFS本身不提供软删除能力,delete() 会永久移除 fs.files 和 fs.chunks 中所有相关文档。一旦调用,数据不可逆丢失,连 MongoDB 的 oplog 都不保留原始内容——这不是“删错了能找回来”的场景,而是“删了就真没了”。
所以必须在应用层接管删除逻辑,核心思路是:不删数据,只改状态。
- 给
fs.files文档加一个deletedAt字段(Date类型),值为null表示未删除 - 所有读取操作(如
find()、openDownloadStream())默认跳过deletedAt: { $ne: null }的文件 - 原生 GridFS API 不识别这个字段,所有封装逻辑必须绕过
GridFSBucket的delete(),改用collection.updateOne()更新fs.files
如何安全地“删除”一个 GridFS 文件
别碰 bucket.delete()。它底层执行的是硬删,且不会检查你有没有加自定义字段。
正确做法是定位到 fs.files 中对应文档,仅更新其元数据:
const filesCollection = db.collection('fs.files');
await filesCollection.updateOne(
{ _id: fileId },
{
$set: { deletedAt: new Date() },
$unset: { metadata: '' } // 可选:临时清空敏感字段,避免误查
}
);
-
fileId必须是ObjectId类型,不是字符串;传错类型会导致静默失败(匹配不到) - 不要在
$set里写deletedAt: '2024-01-01'—— 字符串无法被$ne: null正确判断,后续查询会漏判 - 如果业务需要记录谁删的,可一并写入
deletedBy: userId,但注意fs.files有 16MB 限制,别堆太多字段
怎么从回收站还原文件
还原本质就是把 deletedAt 设回 null,但要注意两个关键点:
- 必须同时检查
deletedAt是否存在且非空,防止重复还原导致字段变成{ deletedAt: null }(MongoDB 会保留该字段,影响后续索引选择) - 还原后,文件立刻对所有读取可见——包括正在运行的流式下载请求,它们不会自动中断,但新请求能拿到
推荐写法:
await filesCollection.updateOne(
{ _id: fileId, deletedAt: { $ne: null } },
{ $set: { deletedAt: null } }
);
这条语句天然具备幂等性:如果文件没删过,$ne: null 条件不成立,update 不生效,不会意外污染数据。
查询时怎么自动过滤已删除文件
所有业务查询(列表、搜索、统计)都必须显式排除已删除项。别指望中间件或全局钩子——GridFS 没这机制。
例如获取用户上传的未删文件列表:
const files = await filesCollection.find({
'metadata.userId': userId,
deletedAt: { $eq: null } // 明确写出来,别省略
}).toArray();
- 索引很重要:在
deletedAt上建稀疏索引{ deletedAt: 1 },能加速$eq: null查询(因为 null 值默认不入普通索引) - 别用
$exists: false判断是否删除——万一有人手动删了字段,行为就不一致了;统一用$eq: null最稳妥 - 如果用 Mongoose,可以在 Schema 中加
virtual或预设queryhook,但底层仍是拼这个条件
真正麻烦的不是代码,是团队里每个人是否都记得在每个 find 地方补上 deletedAt 条件——漏一处,回收站就形同虚设。


















