Skip to content

Commit e8258fd

Browse files
committed
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 <aelhor90@gmail.com>
1 parent 1b42f76 commit e8258fd

3 files changed

Lines changed: 145 additions & 9 deletions

File tree

doc/api/fs.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ longer be used.
180180
<!-- YAML
181181
added: v10.0.0
182182
changes:
183+
- version: REPLACEME
184+
pr-url: https://github.com/nodejs/node/pull/64611
185+
description: The `data` argument supports {Blob}.
183186
- version:
184187
- v21.1.0
185188
- v20.10.0
@@ -196,7 +199,7 @@ changes:
196199
strings anymore.
197200
-->
198201

199-
* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable}
202+
* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable|Blob}
200203
* `options` {Object|string}
201204
* `encoding` {string|null} **Default:** `'utf8'`
202205
* `signal` {AbortSignal|undefined} allows aborting an in-progress writeFile. **Default:** `undefined`
@@ -996,6 +999,9 @@ the end of the file.
996999
<!-- YAML
9971000
added: v10.0.0
9981001
changes:
1002+
- version: REPLACEME
1003+
pr-url: https://github.com/nodejs/node/pull/64611
1004+
description: The `data` argument supports {Blob}.
9991005
- version:
10001006
- v15.14.0
10011007
- v14.18.0
@@ -1007,15 +1013,16 @@ changes:
10071013
strings anymore.
10081014
-->
10091015
1010-
* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable}
1016+
* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable|Blob}
10111017
* `options` {Object|string}
10121018
* `encoding` {string|null} The expected character encoding when `data` is a
10131019
string. **Default:** `'utf8'`
10141020
* `signal` {AbortSignal|undefined} allows aborting an in-progress writeFile. **Default:** `undefined`
10151021
* Returns: {Promise}
10161022
10171023
Asynchronously writes data to a file, replacing the file if it already exists.
1018-
`data` can be a string, a buffer, an {AsyncIterable}, or an {Iterable} object.
1024+
`data` can be a string, a buffer, a {Blob}, an {AsyncIterable}, or an {Iterable}
1025+
object.
10191026
The promise is fulfilled with no arguments upon success.
10201027
10211028
If `options` is a string, then it specifies the `encoding`.
@@ -1233,6 +1240,10 @@ the error raised if the file is not accessible.
12331240
<!-- YAML
12341241
added: v10.0.0
12351242
changes:
1243+
- version: REPLACEME
1244+
pr-url: https://github.com/nodejs/node/pull/64611
1245+
description: The `data` argument now supports typed arrays, `DataView`,
1246+
`AsyncIterable`, `Iterable`, `Stream`, and {Blob}.
12361247
- version:
12371248
- v21.1.0
12381249
- v20.10.0
@@ -1246,7 +1257,7 @@ changes:
12461257
-->
12471258
12481259
* `path` {string|Buffer|URL|FileHandle} filename or {FileHandle}
1249-
* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable}
1260+
* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable|Blob}
12501261
* `options` {Object|string}
12511262
* `encoding` {string|null} **Default:** `'utf8'`
12521263
* `mode` {integer} **Default:** `0o666`
@@ -1256,7 +1267,8 @@ changes:
12561267
* Returns: {Promise} Fulfills with `undefined` upon success.
12571268
12581269
Asynchronously append data to a file, creating the file if it does not yet
1259-
`data` can be a string, a buffer, an {AsyncIterable}, or an {Iterable} object.
1270+
exist. `data` can be a string, a buffer, a {Blob}, an {AsyncIterable}, or an
1271+
{Iterable} object.
12601272
12611273
If `options` is a string, then it specifies the `encoding`.
12621274
@@ -2247,6 +2259,9 @@ All the [caveats][] for `fs.watch()` also apply to `fsPromises.watch()`.
22472259
<!-- YAML
22482260
added: v10.0.0
22492261
changes:
2262+
- version: REPLACEME
2263+
pr-url: https://github.com/nodejs/node/pull/64611
2264+
description: The `data` argument supports {Blob}.
22502265
- version:
22512266
- v21.0.0
22522267
- v20.10.0
@@ -2270,7 +2285,7 @@ changes:
22702285
-->
22712286
22722287
* `file` {string|Buffer|URL|FileHandle} filename or `FileHandle`
2273-
* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable}
2288+
* `data` {string|Buffer|TypedArray|DataView|AsyncIterable|Iterable|Blob}
22742289
* `options` {Object|string}
22752290
* `encoding` {string|null} **Default:** `'utf8'`
22762291
* `mode` {integer} **Default:** `0o666`
@@ -2282,9 +2297,10 @@ changes:
22822297
* Returns: {Promise} Fulfills with `undefined` upon success.
22832298
22842299
Asynchronously writes data to a file, replacing the file if it already exists.
2285-
`data` can be a string, a buffer, an {AsyncIterable}, or an {Iterable} object.
2300+
`data` can be a string, a buffer, a {Blob}, an {AsyncIterable}, or an {Iterable}
2301+
object.
22862302
2287-
The `encoding` option is ignored if `data` is a buffer.
2303+
The `encoding` option is ignored if `data` is a buffer or a {Blob}.
22882304
22892305
If `options` is a string, then it specifies the encoding.
22902306

lib/internal/fs/promises.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const {
5151
},
5252
} = require('internal/errors');
5353
const { isArrayBufferView } = require('internal/util/types');
54+
const { isBlob } = require('internal/blob');
5455

5556
const {
5657
constants: {
@@ -2090,11 +2091,15 @@ async function writeFile(path, data, options) {
20902091

20912092
const flag = options.flag || 'w';
20922093

2093-
if (!isArrayBufferView(data) && !isCustomIterable(data)) {
2094+
if (!isArrayBufferView(data) && !isCustomIterable(data) && !isBlob(data)) {
20942095
validateStringAfterArrayBufferView(data, 'data');
20952096
data = Buffer.from(data, options.encoding || 'utf8');
20962097
}
20972098

2099+
if (isBlob(data)) {
2100+
data = data.stream();
2101+
}
2102+
20982103
validateAbortSignal(options.signal);
20992104
if (path instanceof FileHandle)
21002105
return writeFileHandle(path, data, options.signal, options.encoding);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const fs = require('fs');
5+
const fsPromises = fs.promises;
6+
const path = require('path');
7+
const tmpdir = require('../common/tmpdir');
8+
const assert = require('assert');
9+
const { Blob } = require('buffer');
10+
11+
tmpdir.refresh();
12+
13+
const dest = path.resolve(tmpdir.path, 'tmp.txt');
14+
const otherDest = path.resolve(tmpdir.path, 'tmp-2.txt');
15+
16+
const text = 'Hello, Node.js Core!';
17+
const textBlob = new Blob([text]);
18+
19+
// A binary blob whose content is not valid UTF-8, to ensure binary data is
20+
// preserved verbatim and not corrupted by any encoding handling.
21+
const binaryInput = Buffer.from(
22+
Array.from({ length: 1024 }, (_, i) => i % 256));
23+
const binaryBlob = new Blob([binaryInput]);
24+
25+
// A blob larger than the internal writeFile chunk size so that the chunked
26+
// write loop inside writeFileHandle() is exercised.
27+
const largeString = 'dogs running'.repeat(64 * 1024);
28+
const largeBlob = new Blob([largeString]);
29+
30+
// An empty blob.
31+
const emptyBlob = new Blob([]);
32+
33+
async function doWriteFileBlob() {
34+
await fsPromises.writeFile(dest, textBlob);
35+
const data = fs.readFileSync(dest, 'utf8');
36+
assert.strictEqual(data, text);
37+
}
38+
39+
async function doWriteFileEmptyBlob() {
40+
await fsPromises.writeFile(dest, emptyBlob);
41+
const data = fs.readFileSync(dest);
42+
assert.strictEqual(data.length, 0);
43+
}
44+
45+
async function doWriteFileBinaryBlob() {
46+
await fsPromises.writeFile(dest, binaryBlob);
47+
const data = fs.readFileSync(dest);
48+
assert.deepStrictEqual(data, binaryInput);
49+
}
50+
51+
async function doWriteFileLargeBlob() {
52+
await fsPromises.writeFile(dest, largeBlob);
53+
const data = fs.readFileSync(dest, 'utf8');
54+
assert.strictEqual(data, largeString);
55+
}
56+
57+
async function doAppendFileBlob() {
58+
await fsPromises.writeFile(dest, textBlob);
59+
await fsPromises.appendFile(dest, new Blob([' appended']));
60+
const data = fs.readFileSync(dest, 'utf8');
61+
assert.strictEqual(data, `${text} appended`);
62+
}
63+
64+
async function doFileHandleWriteFileBlob() {
65+
const handle = await fsPromises.open(dest, 'w+');
66+
try {
67+
await handle.writeFile(textBlob);
68+
const data = fs.readFileSync(dest, 'utf8');
69+
assert.strictEqual(data, text);
70+
} finally {
71+
await handle.close();
72+
}
73+
}
74+
75+
async function doFileHandleAppendFileBlob() {
76+
const handle = await fsPromises.open(dest, 'w+');
77+
try {
78+
await handle.writeFile(new Blob(['hello']));
79+
await handle.appendFile(new Blob([' world']));
80+
const data = fs.readFileSync(dest, 'utf8');
81+
assert.strictEqual(data, 'hello world');
82+
} finally {
83+
await handle.close();
84+
}
85+
}
86+
87+
// Binary data must be preserved even when an encoding option is provided,
88+
// because Blob chunks are already typed arrays and the encoding is ignored.
89+
async function doWriteFileBinaryBlobWithEncoding() {
90+
await fsPromises.writeFile(dest, binaryBlob, 'utf8');
91+
const data = fs.readFileSync(dest);
92+
assert.deepStrictEqual(data, binaryInput);
93+
}
94+
95+
async function doWriteBlobWithCancel() {
96+
const controller = new AbortController();
97+
const { signal } = controller;
98+
process.nextTick(() => controller.abort());
99+
await assert.rejects(
100+
fsPromises.writeFile(otherDest, textBlob, { signal }),
101+
{ name: 'AbortError' }
102+
);
103+
}
104+
105+
(async () => {
106+
await doWriteFileBlob();
107+
await doWriteFileEmptyBlob();
108+
await doWriteFileBinaryBlob();
109+
await doWriteFileLargeBlob();
110+
await doAppendFileBlob();
111+
await doFileHandleWriteFileBlob();
112+
await doFileHandleAppendFileBlob();
113+
await doWriteFileBinaryBlobWithEncoding();
114+
await doWriteBlobWithCancel();
115+
})().then(common.mustCall());

0 commit comments

Comments
 (0)