From a033d5bb19cc9da1ae2967b0c6cce9f0900471bc Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Wed, 15 Apr 2026 01:50:00 -0700 Subject: [PATCH] feat(disk-storage): support opt-in flush for durable uploads (#1381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disk storage currently pipes the uploaded file into a vanilla `fs.createWriteStream` and reports success as soon as the stream finishes. On Linux, and any OS with a write-back page cache, this means the bytes can still be sitting in kernel buffers when the request handler returns — a power loss or crash at the wrong moment leaves the file either truncated or zero-length, but the HTTP response already claimed success. Node.js 20.10 / 21.0 added a `flush` option to `fs.createWriteStream` that calls `fdatasync()` on the underlying file descriptor before the stream is closed. This commit exposes it as an opt-in DiskStorage option: multer.diskStorage({ destination: dir, flush: true }) Callers who need durability can turn it on; everything else keeps the current, faster buffered-write behavior so there's no perf regression for existing users. On runtimes older than Node 20.10 the option is silently ignored by `createWriteStream`, which matches the documented upstream behavior. Added two regression tests in `test/disk-storage.js`: 1. With `flush: true`, `createWriteStream` receives `{ flush: true }` and the upload still succeeds. 2. Without `flush`, `createWriteStream` is still called with no options so the default behavior is unchanged. Full `npm test` passes: 74/74. --- storage/disk.js | 11 ++++++++- test/disk-storage.js | 55 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/storage/disk.js b/storage/disk.js index ae1f7bb9..0499a2d4 100644 --- a/storage/disk.js +++ b/storage/disk.js @@ -15,6 +15,14 @@ function getDestination (req, file, cb) { function DiskStorage (opts) { this.getFilename = (opts.filename || getFilename) + // When `flush` is truthy, the disk storage asks the underlying write + // stream to fdatasync() the uploaded file before `_handleFile` reports + // success, so callers that expect the data to survive a crash or power + // loss (see #1381) can rely on it. The option is forwarded as + // `fs.createWriteStream(..., { flush: true })`, which was added in + // Node.js 20.10 / 21.0. On older runtimes the option is ignored and + // behavior falls back to the previous buffered-write semantics. + this.flush = opts.flush === true if (typeof opts.destination === 'string') { fs.mkdirSync(opts.destination, { recursive: true }) @@ -34,7 +42,8 @@ DiskStorage.prototype._handleFile = function _handleFile (req, file, cb) { if (err) return cb(err) var finalPath = path.join(destination, filename) - var outStream = fs.createWriteStream(finalPath) + var writeStreamOptions = that.flush ? { flush: true } : undefined + var outStream = fs.createWriteStream(finalPath, writeStreamOptions) file.stream.pipe(outStream) outStream.on('error', cb) diff --git a/test/disk-storage.js b/test/disk-storage.js index bc923ab5..9f5808ba 100644 --- a/test/disk-storage.js +++ b/test/disk-storage.js @@ -185,4 +185,59 @@ describe('Disk Storage', function () { done() }) }) + + it('should support the flush option for durable uploads (#1381)', function (done) { + var calls = [] + var realCreateWriteStream = fs.createWriteStream + fs.createWriteStream = function (filePath, options) { + calls.push({ filePath: filePath, options: options }) + return realCreateWriteStream.apply(fs, arguments) + } + + var storage = multer.diskStorage({ destination: uploadDir, flush: true }) + var flushUpload = multer({ storage: storage }) + var parser = flushUpload.single('small0') + + var form = new FormData() + form.append('small0', util.file('small0.dat')) + + util.submitForm(parser, form, function (err, req) { + fs.createWriteStream = realCreateWriteStream + assert.ifError(err) + + assert.strictEqual(calls.length, 1) + assert.ok(calls[0].options && calls[0].options.flush === true, + 'expected createWriteStream to be called with { flush: true }') + + assert.strictEqual(req.file.fieldname, 'small0') + assert.strictEqual(req.file.size, 1778) + assert.strictEqual(util.fileSize(req.file.path), 1778) + + done() + }) + }) + + it('should default to buffered writes when flush is not set', function (done) { + var calls = [] + var realCreateWriteStream = fs.createWriteStream + fs.createWriteStream = function (filePath, options) { + calls.push({ filePath: filePath, options: options }) + return realCreateWriteStream.apply(fs, arguments) + } + + var parser = upload.single('small0') + var form = new FormData() + form.append('small0', util.file('small0.dat')) + + util.submitForm(parser, form, function (err, req) { + fs.createWriteStream = realCreateWriteStream + assert.ifError(err) + + assert.strictEqual(calls.length, 1) + assert.strictEqual(calls[0].options, undefined, + 'expected createWriteStream to be called with no options when flush is not set') + + done() + }) + }) })