Skip to content

Commit 39b8c28

Browse files
benjamingrtargos
authored andcommitted
fs: support abortsignal in writeFile
PR-URL: nodejs#35993 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent b1991f2 commit 39b8c28

File tree

5 files changed

+132
-7
lines changed

5 files changed

+132
-7
lines changed

doc/api/fs.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4361,6 +4361,10 @@ details.
43614361
<!-- YAML
43624362
added: v0.1.29
43634363
changes:
4364+
- version: REPLACEME
4365+
pr-url: https://github.com/nodejs/node/pull/35993
4366+
description: The options argument may include an AbortSignal to abort an
4367+
ongoing writeFile request.
43644368
- version: v14.12.0
43654369
pr-url: https://github.com/nodejs/node/pull/34993
43664370
description: The `data` parameter will stringify an object with an
@@ -4395,6 +4399,7 @@ changes:
43954399
* `encoding` {string|null} **Default:** `'utf8'`
43964400
* `mode` {integer} **Default:** `0o666`
43974401
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
4402+
* `signal` {AbortSignal} allows aborting an in-progress writeFile
43984403
* `callback` {Function}
43994404
* `err` {Error}
44004405

@@ -4426,6 +4431,28 @@ It is unsafe to use `fs.writeFile()` multiple times on the same file without
44264431
waiting for the callback. For this scenario, [`fs.createWriteStream()`][] is
44274432
recommended.
44284433

4434+
Similarly to `fs.readFile` - `fs.writeFile` is a convenience method that
4435+
performs multiple `write` calls internally to write the buffer passed to it.
4436+
For performance sensitive code consider using [`fs.createWriteStream()`][].
4437+
4438+
It is possible to use an {AbortSignal} to cancel an `fs.writeFile()`.
4439+
Cancelation is "best effort", and some amount of data is likely still
4440+
to be written.
4441+
4442+
```js
4443+
const controller = new AbortController();
4444+
const { signal } = controller;
4445+
const data = new Uint8Array(Buffer.from('Hello Node.js'));
4446+
fs.writeFile('message.txt', data, { signal }, (err) => {
4447+
// When a request is aborted - the callback is called with an AbortError
4448+
});
4449+
// When the request should be aborted
4450+
controller.abort();
4451+
```
4452+
4453+
Aborting an ongoing request does not abort individual operating
4454+
system requests but rather the internal buffering `fs.writeFile` performs.
4455+
44294456
### Using `fs.writeFile()` with file descriptors
44304457

44314458
When `file` is a file descriptor, the behavior is almost identical to directly
@@ -5670,6 +5697,10 @@ The `atime` and `mtime` arguments follow these rules:
56705697
<!-- YAML
56715698
added: v10.0.0
56725699
changes:
5700+
- version: REPLACEME
5701+
pr-url: https://github.com/nodejs/node/pull/35993
5702+
description: The options argument may include an AbortSignal to abort an
5703+
ongoing writeFile request.
56735704
- version: v14.12.0
56745705
pr-url: https://github.com/nodejs/node/pull/34993
56755706
description: The `data` parameter will stringify an object with an
@@ -5686,6 +5717,7 @@ changes:
56865717
* `encoding` {string|null} **Default:** `'utf8'`
56875718
* `mode` {integer} **Default:** `0o666`
56885719
* `flag` {string} See [support of file system `flags`][]. **Default:** `'w'`.
5720+
* `signal` {AbortSignal} allows aborting an in-progress writeFile
56895721
* Returns: {Promise}
56905722

56915723
Asynchronously writes data to a file, replacing the file if it already exists.
@@ -5699,7 +5731,34 @@ If `options` is a string, then it specifies the encoding.
56995731
Any specified `FileHandle` has to support writing.
57005732

57015733
It is unsafe to use `fsPromises.writeFile()` multiple times on the same file
5702-
without waiting for the `Promise` to be resolved (or rejected).
5734+
without waiting for the `Promise` to be fulfilled (or rejected).
5735+
5736+
Similarly to `fsPromises.readFile` - `fsPromises.writeFile` is a convenience
5737+
method that performs multiple `write` calls internally to write the buffer
5738+
passed to it. For performance sensitive code consider using
5739+
[`fs.createWriteStream()`][].
5740+
5741+
It is possible to use an {AbortSignal} to cancel an `fsPromises.writeFile()`.
5742+
Cancelation is "best effort", and some amount of data is likely still
5743+
to be written.
5744+
5745+
```js
5746+
const controller = new AbortController();
5747+
const { signal } = controller;
5748+
const data = new Uint8Array(Buffer.from('Hello Node.js'));
5749+
(async () => {
5750+
try {
5751+
await fs.writeFile('message.txt', data, { signal });
5752+
} catch (err) {
5753+
// When a request is aborted - err is an AbortError
5754+
}
5755+
})();
5756+
// When the request should be aborted
5757+
controller.abort();
5758+
```
5759+
5760+
Aborting an ongoing request does not abort individual operating
5761+
system requests but rather the internal buffering `fs.writeFile` performs.
57035762

57045763
## FS constants
57055764

lib/fs.js

Lines changed: 22 additions & 4 deletions
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.
@@ -1405,7 +1413,11 @@ function lutimesSync(path, atime, mtime) {
14051413
handleErrorFromBinding(ctx);
14061414
}
14071415

1408-
function writeAll(fd, isUserFd, buffer, offset, length, callback) {
1416+
function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) {
1417+
if (signal?.aborted) {
1418+
callback(lazyDOMException('The operation was aborted', 'AbortError'));
1419+
return;
1420+
}
14091421
// write(fd, buffer, offset, length, position, callback)
14101422
fs.write(fd, buffer, offset, length, null, (writeErr, written) => {
14111423
if (writeErr) {
@@ -1425,7 +1437,7 @@ function writeAll(fd, isUserFd, buffer, offset, length, callback) {
14251437
} else {
14261438
offset += written;
14271439
length -= written;
1428-
writeAll(fd, isUserFd, buffer, offset, length, callback);
1440+
writeAll(fd, isUserFd, buffer, offset, length, signal, callback);
14291441
}
14301442
});
14311443
}
@@ -1442,16 +1454,22 @@ function writeFile(path, data, options, callback) {
14421454

14431455
if (isFd(path)) {
14441456
const isUserFd = true;
1445-
writeAll(path, isUserFd, data, 0, data.byteLength, callback);
1457+
const signal = options.signal;
1458+
writeAll(path, isUserFd, data, 0, data.byteLength, signal, callback);
14461459
return;
14471460
}
14481461

1462+
if (options.signal?.aborted) {
1463+
callback(lazyDOMException('The operation was aborted', 'AbortError'));
1464+
return;
1465+
}
14491466
fs.open(path, flag, options.mode, (openErr, fd) => {
14501467
if (openErr) {
14511468
callback(openErr);
14521469
} else {
14531470
const isUserFd = false;
1454-
writeAll(fd, isUserFd, data, 0, data.byteLength, callback);
1471+
const signal = options.signal;
1472+
writeAll(fd, isUserFd, data, 0, data.byteLength, signal, callback);
14551473
}
14561474
});
14571475
}

lib/internal/fs/promises.js

Lines changed: 8 additions & 2 deletions
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

Lines changed: 12 additions & 0 deletions
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

Lines changed: 30 additions & 0 deletions
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)