From e8258fd6867d9f143276baa9780f4d304b978793 Mon Sep 17 00:00:00 2001 From: Ahmed Elhor Date: Mon, 20 Jul 2026 01:45:56 +0300 Subject: [PATCH] fs: add Blob support to writeFile and appendFile Allow writeFile() and appendFile() (and their FileHandle variants) to accept a Blob, converting it to a web ReadableStream via blob.stream(). Signed-off-by: Ahmed Elhor --- doc/api/fs.md | 32 +++++-- lib/internal/fs/promises.js | 7 +- test/parallel/test-fs-writefile-blob.js | 115 ++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 test/parallel/test-fs-writefile-blob.js diff --git a/doc/api/fs.md b/doc/api/fs.md index 9cb84007b3b815..8e63aacb6ee09e 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -180,6 +180,9 @@ longer be used. -* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable} +* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable|Blob} * `options` {Object|string} * `encoding` {string|null} **Default:** `'utf8'` * `signal` {AbortSignal|undefined} allows aborting an in-progress writeFile. **Default:** `undefined` @@ -996,6 +999,9 @@ the end of the file. -* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable} +* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable|Blob} * `options` {Object|string} * `encoding` {string|null} The expected character encoding when `data` is a string. **Default:** `'utf8'` @@ -1015,7 +1021,8 @@ changes: * Returns: {Promise} Asynchronously writes data to a file, replacing the file if it already exists. -`data` can be a string, a buffer, an {AsyncIterable}, or an {Iterable} object. +`data` can be a string, a buffer, a {Blob}, an {AsyncIterable}, or an {Iterable} +object. The promise is fulfilled with no arguments upon success. If `options` is a string, then it specifies the `encoding`. @@ -1233,6 +1240,10 @@ the error raised if the file is not accessible. * `path` {string|Buffer|URL|FileHandle} filename or {FileHandle} -* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable} +* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable|Blob} * `options` {Object|string} * `encoding` {string|null} **Default:** `'utf8'` * `mode` {integer} **Default:** `0o666` @@ -1256,7 +1267,8 @@ changes: * Returns: {Promise} Fulfills with `undefined` upon success. Asynchronously append data to a file, creating the file if it does not yet -`data` can be a string, a buffer, an {AsyncIterable}, or an {Iterable} object. +exist. `data` can be a string, a buffer, a {Blob}, an {AsyncIterable}, or an +{Iterable} object. If `options` is a string, then it specifies the `encoding`. @@ -2247,6 +2259,9 @@ All the [caveats][] for `fs.watch()` also apply to `fsPromises.watch()`. * `file` {string|Buffer|URL|FileHandle} filename or `FileHandle` -* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable} +* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable|Blob} * `options` {Object|string} * `encoding` {string|null} **Default:** `'utf8'` * `mode` {integer} **Default:** `0o666` @@ -2282,9 +2297,10 @@ changes: * Returns: {Promise} Fulfills with `undefined` upon success. Asynchronously writes data to a file, replacing the file if it already exists. -`data` can be a string, a buffer, an {AsyncIterable}, or an {Iterable} object. +`data` can be a string, a buffer, a {Blob}, an {AsyncIterable}, or an {Iterable} +object. -The `encoding` option is ignored if `data` is a buffer. +The `encoding` option is ignored if `data` is a buffer or a {Blob}. If `options` is a string, then it specifies the encoding. diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 3e336024a15a3c..b65f77ca02eebc 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -51,6 +51,7 @@ const { }, } = require('internal/errors'); const { isArrayBufferView } = require('internal/util/types'); +const { isBlob } = require('internal/blob'); const { constants: { @@ -2090,11 +2091,15 @@ async function writeFile(path, data, options) { const flag = options.flag || 'w'; - if (!isArrayBufferView(data) && !isCustomIterable(data)) { + if (!isArrayBufferView(data) && !isCustomIterable(data) && !isBlob(data)) { validateStringAfterArrayBufferView(data, 'data'); data = Buffer.from(data, options.encoding || 'utf8'); } + if (isBlob(data)) { + data = data.stream(); + } + validateAbortSignal(options.signal); if (path instanceof FileHandle) return writeFileHandle(path, data, options.signal, options.encoding); diff --git a/test/parallel/test-fs-writefile-blob.js b/test/parallel/test-fs-writefile-blob.js new file mode 100644 index 00000000000000..073f8ff3fcee33 --- /dev/null +++ b/test/parallel/test-fs-writefile-blob.js @@ -0,0 +1,115 @@ +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const fsPromises = fs.promises; +const path = require('path'); +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const { Blob } = require('buffer'); + +tmpdir.refresh(); + +const dest = path.resolve(tmpdir.path, 'tmp.txt'); +const otherDest = path.resolve(tmpdir.path, 'tmp-2.txt'); + +const text = 'Hello, Node.js Core!'; +const textBlob = new Blob([text]); + +// A binary blob whose content is not valid UTF-8, to ensure binary data is +// preserved verbatim and not corrupted by any encoding handling. +const binaryInput = Buffer.from( + Array.from({ length: 1024 }, (_, i) => i % 256)); +const binaryBlob = new Blob([binaryInput]); + +// A blob larger than the internal writeFile chunk size so that the chunked +// write loop inside writeFileHandle() is exercised. +const largeString = 'dogs running'.repeat(64 * 1024); +const largeBlob = new Blob([largeString]); + +// An empty blob. +const emptyBlob = new Blob([]); + +async function doWriteFileBlob() { + await fsPromises.writeFile(dest, textBlob); + const data = fs.readFileSync(dest, 'utf8'); + assert.strictEqual(data, text); +} + +async function doWriteFileEmptyBlob() { + await fsPromises.writeFile(dest, emptyBlob); + const data = fs.readFileSync(dest); + assert.strictEqual(data.length, 0); +} + +async function doWriteFileBinaryBlob() { + await fsPromises.writeFile(dest, binaryBlob); + const data = fs.readFileSync(dest); + assert.deepStrictEqual(data, binaryInput); +} + +async function doWriteFileLargeBlob() { + await fsPromises.writeFile(dest, largeBlob); + const data = fs.readFileSync(dest, 'utf8'); + assert.strictEqual(data, largeString); +} + +async function doAppendFileBlob() { + await fsPromises.writeFile(dest, textBlob); + await fsPromises.appendFile(dest, new Blob([' appended'])); + const data = fs.readFileSync(dest, 'utf8'); + assert.strictEqual(data, `${text} appended`); +} + +async function doFileHandleWriteFileBlob() { + const handle = await fsPromises.open(dest, 'w+'); + try { + await handle.writeFile(textBlob); + const data = fs.readFileSync(dest, 'utf8'); + assert.strictEqual(data, text); + } finally { + await handle.close(); + } +} + +async function doFileHandleAppendFileBlob() { + const handle = await fsPromises.open(dest, 'w+'); + try { + await handle.writeFile(new Blob(['hello'])); + await handle.appendFile(new Blob([' world'])); + const data = fs.readFileSync(dest, 'utf8'); + assert.strictEqual(data, 'hello world'); + } finally { + await handle.close(); + } +} + +// Binary data must be preserved even when an encoding option is provided, +// because Blob chunks are already typed arrays and the encoding is ignored. +async function doWriteFileBinaryBlobWithEncoding() { + await fsPromises.writeFile(dest, binaryBlob, 'utf8'); + const data = fs.readFileSync(dest); + assert.deepStrictEqual(data, binaryInput); +} + +async function doWriteBlobWithCancel() { + const controller = new AbortController(); + const { signal } = controller; + process.nextTick(() => controller.abort()); + await assert.rejects( + fsPromises.writeFile(otherDest, textBlob, { signal }), + { name: 'AbortError' } + ); +} + +(async () => { + await doWriteFileBlob(); + await doWriteFileEmptyBlob(); + await doWriteFileBinaryBlob(); + await doWriteFileLargeBlob(); + await doAppendFileBlob(); + await doFileHandleWriteFileBlob(); + await doFileHandleAppendFileBlob(); + await doWriteFileBinaryBlobWithEncoding(); + await doWriteBlobWithCancel(); +})().then(common.mustCall());