Skip to content

Commit 219cd00

Browse files
benjamingrtargos
authored andcommitted
fs: support abortsignal in writeFile
PR-URL: #35993 Backport-PR-URL: #38386 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 5f88b64 commit 219cd00

File tree

5 files changed

+132
-7
lines changed

5 files changed

+132
-7
lines changed

doc/api/fs.md

+60-1
Original file line numberDiff line numberDiff line change
@@ -4375,6 +4375,10 @@ details.
43754375
<!-- YAML
43764376
added: v0.1.29
43774377
changes:
4378+
- version: REPLACEME
4379+
pr-url: https://github.com/nodejs/node/pull/35993
4380+
description: The options argument may include an AbortSignal to abort an
4381+
ongoing writeFile request.
43784382
- version: v14.12.0
43794383
pr-url: https://github.com/nodejs/node/pull/34993
43804384
description: The `data` parameter will stringify an object with an
@@ -4409,6 +4413,7 @@ changes:
44094413
* `encoding` {string|null} **Default:** `'utf8'`
44104414
* `mode` {integer} **Default:** `0o666`
44114415
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
4416+
* `signal` {AbortSignal} allows aborting an in-progress writeFile
44124417
* `callback` {Function}
44134418
* `err` {Error}
44144419

@@ -4440,6 +4445,28 @@ It is unsafe to use `fs.writeFile()` multiple times on the same file without
44404445
waiting for the callback. For this scenario, [`fs.createWriteStream()`][] is
44414446
recommended.
44424447

4448+
Similarly to `fs.readFile` - `fs.writeFile` is a convenience method that
4449+
performs multiple `write` calls internally to write the buffer passed to it.
4450+
For performance sensitive code consider using [`fs.createWriteStream()`][].
4451+
4452+
It is possible to use an {AbortSignal} to cancel an `fs.writeFile()`.
4453+
Cancelation is "best effort", and some amount of data is likely still
4454+
to be written.
4455+
4456+
```js
4457+
const controller = new AbortController();
4458+
const { signal } = controller;
4459+
const data = new Uint8Array(Buffer.from('Hello Node.js'));
4460+
fs.writeFile('message.txt', data, { signal }, (err) => {
4461+
// When a request is aborted - the callback is called with an AbortError
4462+
});
4463+
// When the request should be aborted
4464+
controller.abort();
4465+
```
4466+
4467+
Aborting an ongoing request does not abort individual operating
4468+
system requests but rather the internal buffering `fs.writeFile` performs.
4469+
44434470
### Using `fs.writeFile()` with file descriptors
44444471

44454472
When `file` is a file descriptor, the behavior is almost identical to directly
@@ -5684,6 +5711,10 @@ The `atime` and `mtime` arguments follow these rules:
56845711
<!-- YAML
56855712
added: v10.0.0
56865713
changes:
5714+
- version: REPLACEME
5715+
pr-url: https://github.com/nodejs/node/pull/35993
5716+
description: The options argument may include an AbortSignal to abort an
5717+
ongoing writeFile request.
56875718
- version: v14.12.0
56885719
pr-url: https://github.com/nodejs/node/pull/34993
56895720
description: The `data` parameter will stringify an object with an
@@ -5700,6 +5731,7 @@ changes:
57005731
* `encoding` {string|null} **Default:** `'utf8'`
57015732
* `mode` {integer} **Default:** `0o666`
57025733
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
5734+
* `signal` {AbortSignal} allows aborting an in-progress writeFile
57035735
* Returns: {Promise}
57045736

57055737
Asynchronously writes data to a file, replacing the file if it already exists.
@@ -5713,7 +5745,34 @@ If `options` is a string, then it specifies the encoding.
57135745
Any specified `FileHandle` has to support writing.
57145746

57155747
It is unsafe to use `fsPromises.writeFile()` multiple times on the same file
5716-
without waiting for the `Promise` to be resolved (or rejected).
5748+
without waiting for the `Promise` to be fulfilled (or rejected).
5749+
5750+
Similarly to `fsPromises.readFile` - `fsPromises.writeFile` is a convenience
5751+
method that performs multiple `write` calls internally to write the buffer
5752+
passed to it. For performance sensitive code consider using
5753+
[`fs.createWriteStream()`][].
5754+
5755+
It is possible to use an {AbortSignal} to cancel an `fsPromises.writeFile()`.
5756+
Cancelation is "best effort", and some amount of data is likely still
5757+
to be written.
5758+
5759+
```js
5760+
const controller = new AbortController();
5761+
const { signal } = controller;
5762+
const data = new Uint8Array(Buffer.from('Hello Node.js'));
5763+
(async () => {
5764+
try {
5765+
await fs.writeFile('message.txt', data, { signal });
5766+
} catch (err) {
5767+
// When a request is aborted - err is an AbortError
5768+
}
5769+
})();
5770+
// When the request should be aborted
5771+
controller.abort();
5772+
```
5773+
5774+
Aborting an ongoing request does not abort individual operating
5775+
system requests but rather the internal buffering `fs.writeFile` performs.
57175776

57185777
## FS constants
57195778

lib/fs.js

+22-4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const {
7171
ERR_INVALID_CALLBACK,
7272
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM
7373
},
74+
hideStackFrames,
7475
uvErrmapGet,
7576
uvException
7677
} = require('internal/errors');
@@ -134,6 +135,13 @@ let ReadStream;
134135
let WriteStream;
135136
let rimraf;
136137
let rimrafSync;
138+
let DOMException;
139+
140+
const lazyDOMException = hideStackFrames((message, name) => {
141+
if (DOMException === undefined)
142+
DOMException = internalBinding('messaging').DOMException;
143+
return new DOMException(message, name);
144+
});
137145

138146
// These have to be separate because of how graceful-fs happens to do it's
139147
// monkeypatching.
@@ -1425,7 +1433,11 @@ function lutimesSync(path, atime, mtime) {
14251433
handleErrorFromBinding(ctx);
14261434
}
14271435

1428-
function writeAll(fd, isUserFd, buffer, offset, length, callback) {
1436+
function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
1437+
if (signal?.aborted) {
1438+
callback(lazyDOMException('The operation was aborted', 'AbortError'));
1439+
return;
1440+
}
14291441
// write(fd, buffer, offset, length, position, callback)
14301442
fs.write(fd, buffer, offset, length, null, (writeErr, written) => {
14311443
if (writeErr) {
@@ -1445,7 +1457,7 @@ function writeAll(fd, isUserFd, buffer, offset, length, callback) {
14451457
} else {
14461458
offset += written;
14471459
length -= written;
1448-
writeAll(fd, isUserFd, buffer, offset, length, callback);
1460+
writeAll(fd, isUserFd, buffer, offset, length, signal, callback);
14491461
}
14501462
});
14511463
}
@@ -1462,16 +1474,22 @@ function writeFile(path, data, options, callback) {
14621474

14631475
if (isFd(path)) {
14641476
const isUserFd = true;
1465-
writeAll(path, isUserFd, data, 0, data.byteLength, callback);
1477+
const signal = options.signal;
1478+
writeAll(path, isUserFd, data, 0, data.byteLength, signal, callback);
14661479
return;
14671480
}
14681481

1482+
if (options.signal?.aborted) {
1483+
callback(lazyDOMException('The operation was aborted', 'AbortError'));
1484+
return;
1485+
}
14691486
fs.open(path, flag, options.mode, (openErr, fd) => {
14701487
if (openErr) {
14711488
callback(openErr);
14721489
} else {
14731490
const isUserFd = false;
1474-
writeAll(fd, isUserFd, data, 0, data.byteLength, callback);
1491+
const signal = options.signal;
1492+
writeAll(fd, isUserFd, data, 0, data.byteLength, signal, callback);
14751493
}
14761494
});
14771495
}

lib/internal/fs/promises.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,15 @@ async function fsCall(fn, handle, ...args) {
250250
}
251251
}
252252

253-
async function writeFileHandle(filehandle, data) {
253+
async function writeFileHandle(filehandle, data, signal) {
254254
// `data` could be any kind of typed array.
255255
data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
256256
let remaining = data.length;
257257
if (remaining === 0) return;
258258
do {
259+
if (signal?.aborted) {
260+
throw new lazyDOMException('The operation was aborted', 'AbortError');
261+
}
259262
const { bytesWritten } =
260263
await write(filehandle, data, 0,
261264
MathMin(kWriteFileMaxChunkSize, data.length));
@@ -644,9 +647,12 @@ async function writeFile(path, data, options) {
644647
}
645648

646649
if (path instanceof FileHandle)
647-
return writeFileHandle(path, data);
650+
return writeFileHandle(path, data, options.signal);
648651

649652
const fd = await open(path, flag, options.mode);
653+
if (options.signal?.aborted) {
654+
throw new lazyDOMException('The operation was aborted', 'AbortError');
655+
}
650656
return PromisePrototypeFinally(writeFileHandle(fd, data), fd.close);
651657
}
652658

test/parallel/test-fs-promises-writefile.js

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --experimental-abortcontroller
12
'use strict';
23

34
const common = require('../common');
@@ -11,6 +12,7 @@ const tmpDir = tmpdir.path;
1112
tmpdir.refresh();
1213

1314
const dest = path.resolve(tmpDir, 'tmp.txt');
15+
const otherDest = path.resolve(tmpDir, 'tmp-2.txt');
1416
const buffer = Buffer.from('abc'.repeat(1000));
1517
const buffer2 = Buffer.from('xyz'.repeat(1000));
1618

@@ -20,6 +22,15 @@ async function doWrite() {
2022
assert.deepStrictEqual(data, buffer);
2123
}
2224

25+
async function doWriteWithCancel() {
26+
const controller = new AbortController();
27+
const { signal } = controller;
28+
process.nextTick(() => controller.abort());
29+
assert.rejects(fsPromises.writeFile(otherDest, buffer, { signal }), {
30+
name: 'AbortError'
31+
});
32+
}
33+
2334
async function doAppend() {
2435
await fsPromises.appendFile(dest, buffer2);
2536
const data = fs.readFileSync(dest);
@@ -41,6 +52,7 @@ async function doReadWithEncoding() {
4152
}
4253

4354
doWrite()
55+
.then(doWriteWithCancel)
4456
.then(doAppend)
4557
.then(doRead)
4658
.then(doReadWithEncoding)

test/parallel/test-fs-write-file.js

+30
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

22+
// Flags: --experimental-abortcontroller
2223
'use strict';
2324
const common = require('../common');
2425
const assert = require('assert');
@@ -66,3 +67,32 @@ fs.open(filename4, 'w+', common.mustSucceed((fd) => {
6667
}));
6768
}));
6869
}));
70+
71+
72+
{
73+
// Test that writeFile is cancellable with an AbortSignal.
74+
// Before the operation has started
75+
const controller = new AbortController();
76+
const signal = controller.signal;
77+
const filename3 = join(tmpdir.path, 'test3.txt');
78+
79+
fs.writeFile(filename3, s, { signal }, common.mustCall((err) => {
80+
assert.strictEqual(err.name, 'AbortError');
81+
}));
82+
83+
controller.abort();
84+
}
85+
86+
{
87+
// Test that writeFile is cancellable with an AbortSignal.
88+
// After the operation has started
89+
const controller = new AbortController();
90+
const signal = controller.signal;
91+
const filename4 = join(tmpdir.path, 'test4.txt');
92+
93+
fs.writeFile(filename4, s, { signal }, common.mustCall((err) => {
94+
assert.strictEqual(err.name, 'AbortError');
95+
}));
96+
97+
process.nextTick(() => controller.abort());
98+
}

0 commit comments

Comments
 (0)