GridFS 默认不支持内容去重,需自行实现全文件哈希(如SHA-256)比对逻辑:上传前计算哈希并查询fs.files中contentHash字段,存在则复用_id,否则上传并写入哈希值,同时创建唯一索引防止重复。

GridFS 默认不支持内容去重,必须自己实现哈希比对逻辑
GridFS 本身只是把大文件切块(chunks)存进两个集合(fs.files 和 fs.chunks),它不做任何内容相似性判断。哪怕你上传完全相同的文件两次,GridFS 会生成两套独立的 file_id 和所有 chunk 记录——这是默认行为,不是 bug。
要实现“相同内容只存一份”,核心是在写入前计算整个文件的哈希值(比如 SHA-256),然后查库:如果该哈希已存在,就跳过存储,直接复用已有 _id;否则才走完整 GridFS 写入流程。
注意:不能只比对单个 chunk 的哈希——因为不同文件可能有相同 chunk(尤其压缩包、加密容器),但整体内容不同;必须是**全文件哈希**,且算法要稳定(同一文件每次计算结果一致)。
用 sha256 做文件级哈希,并扩展到 fs.files 集合
GridFS 的 fs.files 文档默认不含哈希字段,你需要主动加一个,比如叫 contentHash。写入前先算哈希:
const crypto = require('crypto');
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
然后查询是否已存在:
const existing = await db.collection('fs.files').findOne({ contentHash: hash });
如果 existing 存在,就不用调 bucket.uploadFromStream(),直接返回 existing._id 即可。如果不存在,再上传,并在 metadata 里带上哈希:
bucket.uploadFromStream(filename, stream, { metadata: { contentHash: hash } })- 或者上传后手动更新:
db.collection('fs.files').updateOne({ _id: fileId }, { $set: { contentHash: hash } })
别漏掉索引——否则查哈希会慢:db.fs.files.createIndex({ contentHash: 1 }, { unique: true })。加 unique: true 能防止并发写入时重复插入(会抛 E11000 duplicate key 错误,需捕获处理)。
避免在上传中途计算哈希导致内存或流中断
常见错误是:用 fs.readFileSync() 读整个文件再哈希——大文件(>100MB)直接 OOM;或者边读流边上传、边哈希,但没同步好,导致哈希值错位或流被消费两次。
正确做法是用可复用的流 + stream.PassThrough 分叉:
const { PassThrough } = require('stream');
const hashStream = crypto.createHash('sha256');
const passthrough = new PassThrough();
readStream.pipe(passthrough);
readStream.pipe(hashStream);
// 等流结束再取哈希
readStream.on('end', () => {
const hash = hashStream.digest('hex');
// 后续上传用 passthrough
bucket.uploadFromStream(filename, passthrough, { metadata: { contentHash: hash } });
});
关键点:
- 必须让原始
readStream同时流向哈希流和 passthrough,不能先后 pipe -
passthrough是可重读的流副本,GridFS upload 才能正常消费 - 不要用
stream.clone()(Node.js 原生不支持),也不要依赖第三方库做流复制
chunk 级哈希只适合高级场景,如增量同步或差量备份
全文件哈希能解决“完全相同文件”的去重,但没法识别“只改了一页的 PDF”或“新增一行的日志”。这时需要 chunk 级哈希——即把文件按固定大小(如 256KB)切块,每块算 SHA-256,再比对哪些 chunk 已存在。
但这不是 GridFS 原生能力,你要自己管理 chunk 映射表(比如新建 fs.chunk_hashes 集合,存 { hash: "...", chunkIndex: 0, fileId: ObjectId(...) })。而且要注意:
- GridFS 默认 chunk 大小是 255KB,但你不能直接复用
fs.chunks里的data字段哈希——因为 BSON 二进制字段可能带 padding 或编码差异 - 必须用原始分块逻辑(同参数、同起始偏移)重新切文件,再逐块哈希,否则无法对齐
- 合并时需重建
fs.chunks文档的n字段顺序,否则读出来乱序
实际项目中,95% 的去重需求靠全文件哈希就够了;chunk 级方案复杂度高、维护成本大,只在明确需要 delta sync 的场景才值得投入。











