Skip to content

Commit ccf9f0e

Browse files
benjamingrdanielleadams
authored andcommitted
fs: support abortsignal in writeFile
PR-URL: #35993 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent dcb2760 commit ccf9f0e

5 files changed

Lines changed: 130 additions & 7 deletions

File tree

‎doc/api/fs.md‎

Lines changed: 60 additions & 1 deletion
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+
constcontroller=newAbortController();
4468+
const { signal } = controller;
4469+
constdata=newUint8Array(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+
constcontroller=newAbortController();
5794+
const { signal } = controller;
5795+
constdata=newUint8Array(Buffer.from('Hello Node.js'));
5796+
(async () => {
5797+
try {
5798+
awaitfs.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‎

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
letWriteStream;
134135
letrimraf;
135136
letrimrafSync;
137+
letDOMException;
138+
139+
constlazyDOMException=hideStackFrames((message,name)=>{
140+
if(DOMException===undefined)
141+
DOMException=internalBinding('messaging').DOMException;
142+
returnnewDOMException(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-
functionwriteAll(fd,isUserFd,buffer,offset,length,callback){
1420+
functionwriteAll(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
constisUserFd=true;
1449-
writeAll(path,isUserFd,data,0,data.byteLength,callback);
1461+
constsignal=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
constisUserFd=false;
1458-
writeAll(fd,isUserFd,data,0,data.byteLength,callback);
1475+
constsignal=options.signal;
1476+
writeAll(fd,isUserFd,data,0,data.byteLength,signal,callback);
14591477
}
14601478
});
14611479
}

‎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-
asyncfunctionwriteFileHandle(filehandle,data){
253+
asyncfunctionwriteFileHandle(filehandle,data,signal){
254254
// `data` could be any kind of typed array.
255255
data=newUint8Array(data.buffer,data.byteOffset,data.byteLength);
256256
letremaining=data.length;
257257
if(remaining===0)return;
258258
do{
259+
if(signal?.aborted){
260+
thrownewlazyDOMException('The operation was aborted','AbortError');
261+
}
259262
const{ bytesWritten }=
260263
awaitwrite(filehandle,data,0,
261264
MathMin(kWriteFileMaxChunkSize,data.length));
@@ -633,9 +636,12 @@ async function writeFile(path, data, options) {
633636
}
634637

635638
if(pathinstanceofFileHandle)
636-
returnwriteFileHandle(path,data);
639+
returnwriteFileHandle(path,data,options.signal);
637640

638641
constfd=awaitopen(path,flag,options.mode);
642+
if(options.signal?.aborted){
643+
thrownewlazyDOMException('The operation was aborted','AbortError');
644+
}
639645
returnPromisePrototypeFinally(writeFileHandle(fd,data),fd.close);
640646
}
641647

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const tmpDir = tmpdir.path;
1111
tmpdir.refresh();
1212

1313
constdest=path.resolve(tmpDir,'tmp.txt');
14+
constotherDest=path.resolve(tmpDir,'tmp-2.txt');
1415
constbuffer=Buffer.from('abc'.repeat(1000));
1516
constbuffer2=Buffer.from('xyz'.repeat(1000));
1617

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

24+
asyncfunctiondoWriteWithCancel(){
25+
constcontroller=newAbortController();
26+
const{ signal }=controller;
27+
process.nextTick(()=>controller.abort());
28+
assert.rejects(fsPromises.writeFile(otherDest,buffer,{ signal }),{
29+
name: 'AbortError'
30+
});
31+
}
32+
2333
asyncfunctiondoAppend(){
2434
awaitfsPromises.appendFile(dest,buffer2);
2535
constdata=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‎

Lines changed: 29 additions & 0 deletions
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+
constcontroller=newAbortController();
75+
constsignal=controller.signal;
76+
constfilename3=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+
constcontroller=newAbortController();
89+
constsignal=controller.signal;
90+
constfilename4=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)