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() + }) + }) })