Skip to content

Commit 017246b

Browse files
committed
fs: support abortsignal in writeFile
1 parent da53a3c commit 017246b

File tree

5 files changed

+130
-7
lines changed

5 files changed

+130
-7
lines changed

doc/api/fs.md

+60-1
Original file line numberDiff line numberDiff line change
@@ -4385,6 +4385,10 @@ details.
43854385
<!-- YAML
43864386
added: v0.1.29
43874387
changes:
4388+
- version: REPLACEME
4389+
pr-url: https://github.com/nodejs/node/pull/35993
4390+
description: The options argument may include an AbortSignal to abort an
4391+
ongoing writeFile request.
43884392
- version: v14.12.0
43894393
pr-url: https://github.com/nodejs/node/pull/34993
43904394
description: The `data` parameter will stringify an object with an
@@ -4419,6 +4423,7 @@ changes:
44194423
* `encoding` {string|null} **Default:** `'utf8'`
44204424
* `mode` {integer} **Default:** `0o666`
44214425
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
4426+
* `signal` {AbortSignal} allows aborting an in-progress writeFile
44224427
* `callback` {Function}
44234428
* `err` {Error}
44244429

@@ -4450,6 +4455,28 @@ It is unsafe to use `fs.writeFile()` multiple times on the same file without
44504455
waiting for the callback. For this scenario, [`fs.createWriteStream()`][] is
44514456
recommended.
44524457

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

44554482
When `file` is a file descriptor, the behavior is almost identical to directly
@@ -5717,6 +5744,10 @@ The `atime` and `mtime` arguments follow these rules:
57175744
<!-- YAML
57185745
added: v10.0.0
57195746
changes:
5747+
- version: REPLACEME
5748+
pr-url: https://github.com/nodejs/node/pull/35993
5749+
description: The options argument may include an AbortSignal to abort an
5750+
ongoing writeFile request.
57205751
- version: v14.12.0
57215752
pr-url: https://github.com/nodejs/node/pull/34993
57225753
description: The `data` parameter will stringify an object with an
@@ -5733,6 +5764,7 @@ changes:
57335764
* `encoding` {string|null} **Default:** `'utf8'`
57345765
* `mode` {integer} **Default:** `0o666`
57355766
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
5767+
* `signal` {AbortSignal} allows aborting an in-progress writeFile
57365768
* Returns: {Promise}
57375769

57385770
Asynchronously writes data to a file, replacing the file if it already exists.
@@ -5746,7 +5778,34 @@ If `options` is a string, then it specifies the encoding.
57465778
Any specified `FileHandle` has to support writing.
57475779

57485780
It is unsafe to use `fsPromises.writeFile()` multiple times on the same file
5749-
without waiting for the `Promise` to be resolved (or rejected).
5781+
without waiting for the `Promise` to be fulfilled (or rejected).
5782+
5783+
Similarly to `fsPromises.readFile` - `fsPromises.writeFile` is a convenience
5784+
method that performs multiple `write` calls internally to write the buffer
5785+
passed to it. For performance sensitive code consider using
5786+
[`fs.createWriteStream()`][].
5787+
5788+
It is possible to use an {AbortSignal} to cancel an `fsPromises.writeFile()`.
5789+
Cancelation is "best effort", and some amount of data is likely still
5790+
to be written.
5791+
5792+
```js
5793+
const controller = new AbortController();
5794+
const { signal } = controller;
5795+
const data = new Uint8Array(Buffer.from('Hello Node.js'));
5796+
(async () => {
5797+
try {
5798+
await fs.writeFile('message.txt', data, { signal });
5799+
} catch (err) {
5800+
// When a request is aborted - err is an AbortError
5801+
}
5802+
})();
5803+
// When the request should be aborted
5804+
controller.abort();
5805+
```
5806+
5807+
Aborting an ongoing request does not abort individual operating
5808+
system requests but rather the internal buffering `fs.writeFile` performs.
57505809

57515810
## FS constants
57525811

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
uvException
7576
} = require('internal/errors');
7677

@@ -133,6 +134,13 @@ let ReadStream;
133134
let WriteStream;
134135
let rimraf;
135136
let rimrafSync;
137+
let DOMException;
138+
139+
const lazyDOMException = hideStackFrames((message, name) => {
140+
if (DOMException === undefined)
141+
DOMException = internalBinding('messaging').DOMException;
142+
return new DOMException(message, name);
143+
});
136144

137145
// These have to be separate because of how graceful-fs happens to do it's
138146
// monkeypatching.
@@ -1409,7 +1417,11 @@ function lutimesSync(path, atime, mtime) {
14091417
handleErrorFromBinding(ctx);
14101418
}
14111419

1412-
function writeAll(fd, isUserFd, buffer, offset, length, callback) {
1420+
function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
1421+
if (signal?.aborted) {
1422+
callback(lazyDOMException('The operation was aborted', 'AbortError'));
1423+
return;
1424+
}
14131425
// write(fd, buffer, offset, length, position, callback)
14141426
fs.write(fd, buffer, offset, length, null, (writeErr, written) => {
14151427
if (writeErr) {
@@ -1429,7 +1441,7 @@ function writeAll(fd, isUserFd, buffer, offset, length, callback) {
14291441
} else {
14301442
offset += written;
14311443
length -= written;
1432-
writeAll(fd, isUserFd, buffer, offset, length, callback);
1444+
writeAll(fd, isUserFd, buffer, offset, length, signal, callback);
14331445
}
14341446
});
14351447
}
@@ -1446,16 +1458,22 @@ function writeFile(path, data, options, callback) {
14461458

14471459
if (isFd(path)) {
14481460
const isUserFd = true;
1449-
writeAll(path, isUserFd, data, 0, data.byteLength, callback);
1461+
const signal = options.signal;
1462+
writeAll(path, isUserFd, data, 0, data.byteLength, signal, callback);
14501463
return;
14511464
}
14521465

1466+
if (options.signal?.aborted) {
1467+
callback(lazyDOMException('The operation was aborted', 'AbortError'));
1468+
return;
1469+
}
14531470
fs.open(path, flag, options.mode, (openErr, fd) => {
14541471
if (openErr) {
14551472
callback(openErr);
14561473
} else {
14571474
const isUserFd = false;
1458-
writeAll(fd, isUserFd, data, 0, data.byteLength, callback);
1475+
const signal = options.signal;
1476+
writeAll(fd, isUserFd, data, 0, data.byteLength, signal, callback);
14591477
}
14601478
});
14611479
}

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));
@@ -633,9 +636,12 @@ async function writeFile(path, data, options) {
633636
}
634637

635638
if (path instanceof FileHandle)
636-
return writeFileHandle(path, data);
639+
return writeFileHandle(path, data, options.signal);
637640

638641
const fd = await open(path, flag, options.mode);
642+
if (options.signal?.aborted) {
643+
throw new lazyDOMException('The operation was aborted', 'AbortError');
644+
}
639645
return PromisePrototypeFinally(writeFileHandle(fd, data), fd.close);
640646
}
641647

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

+11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const tmpDir = tmpdir.path;
1111
tmpdir.refresh();
1212

1313
const dest = path.resolve(tmpDir, 'tmp.txt');
14+
const otherDest = path.resolve(tmpDir, 'tmp-2.txt');
1415
const buffer = Buffer.from('abc'.repeat(1000));
1516
const buffer2 = Buffer.from('xyz'.repeat(1000));
1617

@@ -20,6 +21,15 @@ async function doWrite() {
2021
assert.deepStrictEqual(data, buffer);
2122
}
2223

24+
async function doWriteWithCancel() {
25+
const controller = new AbortController();
26+
const { signal } = controller;
27+
process.nextTick(() => controller.abort());
28+
assert.rejects(fsPromises.writeFile(otherDest, buffer, { signal }), {
29+
name: 'AbortError'
30+
});
31+
}
32+
2333
async function doAppend() {
2434
await fsPromises.appendFile(dest, buffer2);
2535
const data = fs.readFileSync(dest);
@@ -41,6 +51,7 @@ async function doReadWithEncoding() {
4151
}
4252

4353
doWrite()
54+
.then(doWriteWithCancel)
4455
.then(doAppend)
4556
.then(doRead)
4657
.then(doReadWithEncoding)

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

+29
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,32 @@ fs.open(filename4, 'w+', common.mustSucceed((fd) => {
6666
}));
6767
}));
6868
}));
69+
70+
71+
{
72+
// Test that writeFile is cancellable with an AbortSignal.
73+
// Before the operation has started
74+
const controller = new AbortController();
75+
const signal = controller.signal;
76+
const filename3 = join(tmpdir.path, 'test3.txt');
77+
78+
fs.writeFile(filename3, s, { signal }, common.mustCall((err) => {
79+
assert.strictEqual(err.name, 'AbortError');
80+
}));
81+
82+
controller.abort();
83+
}
84+
85+
{
86+
// Test that writeFile is cancellable with an AbortSignal.
87+
// After the operation has started
88+
const controller = new AbortController();
89+
const signal = controller.signal;
90+
const filename4 = join(tmpdir.path, 'test4.txt');
91+
92+
fs.writeFile(filename4, s, { signal }, common.mustCall((err) => {
93+
assert.strictEqual(err.name, 'AbortError');
94+
}));
95+
96+
process.nextTick(() => controller.abort());
97+
}

0 commit comments

Comments
 (0)