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 <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 5f88b64 commit 219cd00

5 files changed

Lines changed: 132 additions & 7 deletions

File tree

‎doc/api/fs.md‎

Lines changed: 60 additions & 1 deletion
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+
constcontroller=newAbortController();
4458+
const { signal } = controller;
4459+
constdata=newUint8Array(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+
constcontroller=newAbortController();
5761+
const { signal } = controller;
5762+
constdata=newUint8Array(Buffer.from('Hello Node.js'));
5763+
(async () => {
5764+
try {
5765+
awaitfs.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‎

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
uvErrmapGet,
7576
uvException
7677
}=require('internal/errors');
@@ -134,6 +135,13 @@ let ReadStream;
134135
letWriteStream;
135136
letrimraf;
136137
letrimrafSync;
138+
letDOMException;
139+
140+
constlazyDOMException=hideStackFrames((message,name)=>{
141+
if(DOMException===undefined)
142+
DOMException=internalBinding('messaging').DOMException;
143+
returnnewDOMException(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-
functionwriteAll(fd,isUserFd,buffer,offset,length,callback){
1436+
functionwriteAll(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
constisUserFd=true;
1465-
writeAll(path,isUserFd,data,0,data.byteLength,callback);
1477+
constsignal=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
constisUserFd=false;
1474-
writeAll(fd,isUserFd,data,0,data.byteLength,callback);
1491+
constsignal=options.signal;
1492+
writeAll(fd,isUserFd,data,0,data.byteLength,signal,callback);
14751493
}
14761494
});
14771495
}

‎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));
@@ -644,9 +647,12 @@ async function writeFile(path, data, options) {
644647
}
645648

646649
if(pathinstanceofFileHandle)
647-
returnwriteFileHandle(path,data);
650+
returnwriteFileHandle(path,data,options.signal);
648651

649652
constfd=awaitopen(path,flag,options.mode);
653+
if(options.signal?.aborted){
654+
thrownewlazyDOMException('The operation was aborted','AbortError');
655+
}
650656
returnPromisePrototypeFinally(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
constcommon=require('../common');
@@ -11,6 +12,7 @@ const tmpDir = tmpdir.path;
1112
tmpdir.refresh();
1213

1314
constdest=path.resolve(tmpDir,'tmp.txt');
15+
constotherDest=path.resolve(tmpDir,'tmp-2.txt');
1416
constbuffer=Buffer.from('abc'.repeat(1000));
1517
constbuffer2=Buffer.from('xyz'.repeat(1000));
1618

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

25+
asyncfunctiondoWriteWithCancel(){
26+
constcontroller=newAbortController();
27+
const{ signal }=controller;
28+
process.nextTick(()=>controller.abort());
29+
assert.rejects(fsPromises.writeFile(otherDest,buffer,{ signal }),{
30+
name: 'AbortError'
31+
});
32+
}
33+
2334
asyncfunctiondoAppend(){
2435
awaitfsPromises.appendFile(dest,buffer2);
2536
constdata=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
constcommon=require('../common');
2425
constassert=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+
constcontroller=newAbortController();
76+
constsignal=controller.signal;
77+
constfilename3=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+
constcontroller=newAbortController();
90+
constsignal=controller.signal;
91+
constfilename4=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)