From 6a9bae96b9b472c9b290d89c691b5752c81ca5ed Mon Sep 17 00:00:00 2001 From: Nikolai Ovtsinnikov Date: Fri, 13 Sep 2024 10:12:49 +0300 Subject: [PATCH] on attachment upload calculate file content hash --- lib/attachments/gridstore-storage.js | 68 +++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/lib/attachments/gridstore-storage.js b/lib/attachments/gridstore-storage.js index 87bb5020..4498496c 100644 --- a/lib/attachments/gridstore-storage.js +++ b/lib/attachments/gridstore-storage.js @@ -7,6 +7,7 @@ const errors = require('../errors'); const log = require('npmlog'); const crypto = require('crypto'); const base64Offset = require('./base64-offset'); +const Transform = require('stream').Transform; // Set to false to disable base64 decoding feature const FEATURE_DECODE_ATTACHMENTS = true; @@ -14,6 +15,40 @@ const FEATURE_DECODE_ATTACHMENTS = true; const ORPHANED_ATTACHMENTS_DELAY = 24 * 3600 * 1000; const MAX_ORPHANED_ATTACHMENTS = 1000; +class FileHashCalculator extends Transform { + constructor(options) { + super(options); + this.bodyHash = crypto.createHash('sha256'); + this.hash = null; + } + + updateHash(chunk) { + this.bodyHash.update(chunk); + } + + _transform(chunk, encoding, callback) { + if (!chunk || !chunk.length) { + return callback(); + } + + if (typeof chunk === 'string') { + chunk = Buffer.from(chunk, encoding); + } + + this.updateHash(chunk); + this.push(chunk); + + callback(); + } + + _flush(done) { + this.hash = this.bodyHash.digest('base64'); + done(); + } +} + +let fileHashCalculator = new FileHashCalculator(); + class GridstoreStorage { constructor(options) { this.bucketName = (options.options && options.options.bucket) || 'attachments'; @@ -129,6 +164,35 @@ class GridstoreStorage { let storeLock; let attachmentCallback = (...args) => { + // store finished uploading, add the hash of the file contents to file metadata + if (args.length > 2) { + const calculatedFileContentHash = args[2]; + + this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate( + { + _id: hash + }, + { + $set: { + 'metadata.fileContentHash': calculatedFileContentHash + } + }, + { + returnDocument: 'after' + }, + (err, res) => { + if (err) { + return attachmentCallback(err); + } + + if (res && res.value && res.value.metadata.fileContentHash && res.value.metadata.fileContentHash === calculatedFileContentHash) { + // all good? + // do nothing + } + } + ); + } + if (storeLock) { log.silly('GridStore', '[%s] UNLOCK lock=%s status=%s', instance, lockId, storeLock.success ? 'locked' : 'empty'); if (storeLock.success) { @@ -282,13 +346,13 @@ class GridstoreStorage { attachmentCallback(err); }); - store.once('finish', () => attachmentCallback(null, id)); + store.once('finish', () => attachmentCallback(null, id, fileHashCalculator.hash)); if (!metadata.decoded) { store.end(attachment.body); } else { let decoder = new libbase64.Decoder(); - decoder.pipe(store); + decoder.pipe(fileHashCalculator).pipe(store); decoder.once('error', err => { // pass error forward store.emit('error', err);