Skip to content

Commit 5f88b64

Browse files
benjamingrtargos
authored andcommitted
fs: add support for AbortSignal in readFile
PR-URL: #35911 Backport-PR-URL: #38386 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 88f4261 commit 5f88b64

File tree

7 files changed

+146
-14
lines changed

7 files changed

+146
-14
lines changed

‎doc/api/fs.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3026,6 +3026,10 @@ If `options.withFileTypes` is set to `true`, the result will contain
30263026
<!-- YAML
30273027
added: v0.1.29
30283028
changes:
3029+
- version: REPLACEME
3030+
pr-url: https://github.com/nodejs/node/pull/35911
3031+
description: The options argument may include an AbortSignal to abort an
3032+
ongoing readFile request.
30293033
- version: v10.0.0
30303034
pr-url: https://github.com/nodejs/node/pull/12562
30313035
description: The `callback` parameter is no longer optional. Not passing
@@ -3051,6 +3055,7 @@ changes:
30513055
*`options`{Object|string}
30523056
*`encoding`{string|null} **Default:**`null`
30533057
*`flag`{string} See [support of file system `flags`][]. **Default:**`'r'`.
3058+
*`signal`{AbortSignal} allows aborting an in-progress readFile
30543059
*`callback`{Function}
30553060
*`err`{Error}
30563061
*`data`{string|Buffer}
@@ -3092,9 +3097,25 @@ fs.readFile('<directory>', (err, data) =>{
30923097
});
30933098
```
30943099

3100+
It is possible to abort an ongoing request using an `AbortSignal`. If a
3101+
request is aborted the callback is called with an `AbortError`:
3102+
3103+
```js
3104+
constcontroller=newAbortController();
3105+
constsignal=controller.signal;
3106+
fs.readFile(fileInfo[0].name,{signal }, (err, buf) =>{
3107+
// ...
3108+
});
3109+
// When you want to abort the request
3110+
controller.abort();
3111+
```
3112+
30953113
The `fs.readFile()` function buffers the entire file. To minimize memory costs,
30963114
when possible prefer streaming via `fs.createReadStream()`.
30973115

3116+
Aborting an ongoing request does not abort individual operating
3117+
system requests but rather the internal buffering `fs.readFile` performs.
3118+
30983119
### File descriptors
30993120

31003121
1. Any specified file descriptor has to support reading.
@@ -4748,6 +4769,7 @@ added: v10.0.0
47484769

47494770
*`options`{Object|string}
47504771
*`encoding`{string|null} **Default:**`null`
4772+
*`signal`{AbortSignal} allows aborting an in-progress readFile
47514773
* Returns:{Promise}
47524774

47534775
Asynchronously reads the entire contents of a file.
@@ -5411,12 +5433,18 @@ print('./').catch(console.error);
54115433
### `fsPromises.readFile(path[, options])`
54125434
<!-- YAML
54135435
added: v10.0.0
5436+
changes:
5437+
- version: REPLACEME
5438+
pr-url: https://github.com/nodejs/node/pull/35911
5439+
description: The options argument may include an AbortSignal to abort an
5440+
ongoing readFile request.
54145441
-->
54155442

54165443
*`path`{string|Buffer|URL|FileHandle} filename or `FileHandle`
54175444
*`options`{Object|string}
54185445
*`encoding`{string|null} **Default:**`null`
54195446
*`flag`{string} See [support of file system `flags`][]. **Default:**`'r'`.
5447+
*`signal`{AbortSignal} allows aborting an in-progress readFile
54205448
* Returns:{Promise}
54215449

54225450
Asynchronously reads the entire contents of a file.
@@ -5432,6 +5460,20 @@ platform-specific. On macOS, Linux, and Windows, the promise will be rejected
54325460
with an error. On FreeBSD, a representation of the directory's contents will be
54335461
returned.
54345462

5463+
It is possible to abort an ongoing `readFile` using an `AbortSignal`. If a
5464+
request is aborted the promise returned is rejected with an `AbortError`:
5465+
5466+
```js
5467+
constcontroller=newAbortController();
5468+
constsignal=controller.signal;
5469+
readFile(fileName,{signal }).then((file) =>{/* ... */ });
5470+
// Abort the request
5471+
controller.abort();
5472+
```
5473+
5474+
Aborting an ongoing request does not abort individual operating
5475+
system requests but rather the internal buffering `fs.readFile` performs.
5476+
54355477
Any specified `FileHandle` has to support reading.
54365478

54375479
### `fsPromises.readlink(path[, options])`

‎lib/fs.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ function readFile(path, options, callback){
316316
constcontext=newReadFileContext(callback,options.encoding);
317317
context.isUserFd=isFd(path);// File descriptor ownership
318318

319+
if(options.signal){
320+
context.signal=options.signal;
321+
}
319322
if(context.isUserFd){
320323
process.nextTick(functiontick(context){
321324
readFileAfterOpen.call({ context },null,path);

‎lib/internal/fs/promises.js‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ const{
3030
}=internalBinding('constants').fs;
3131
constbinding=internalBinding('fs');
3232
const{ Buffer }=require('buffer');
33+
34+
const{ codes, hideStackFrames }=require('internal/errors');
3335
const{
3436
ERR_FS_FILE_TOO_LARGE,
3537
ERR_INVALID_ARG_TYPE,
3638
ERR_INVALID_ARG_VALUE,
37-
ERR_METHOD_NOT_IMPLEMENTED
38-
}=require('internal/errors').codes;
39+
ERR_METHOD_NOT_IMPLEMENTED,
40+
}=codes;
3941
const{ isArrayBufferView }=require('internal/util/types');
4042
const{ rimrafPromises }=require('internal/fs/rimraf');
4143
const{
@@ -83,6 +85,13 @@ const{
8385
constgetDirectoryEntriesPromise=promisify(getDirents);
8486
constvalidateRmOptionsPromise=promisify(validateRmOptions);
8587

88+
letDOMException;
89+
constlazyDOMException=hideStackFrames((message,name)=>{
90+
if(DOMException===undefined)
91+
DOMException=internalBinding('messaging').DOMException;
92+
returnnewDOMException(message,name);
93+
});
94+
8695
classFileHandleextendsJSTransferable{
8796
constructor(filehandle){
8897
super();
@@ -260,8 +269,17 @@ async function writeFileHandle(filehandle, data){
260269
}
261270

262271
asyncfunctionreadFileHandle(filehandle,options){
272+
constsignal=options&&options.signal;
273+
274+
if(signal&&signal.aborted){
275+
throwlazyDOMException('The operation was aborted','AbortError');
276+
}
263277
conststatFields=awaitbinding.fstat(filehandle.fd,false,kUsePromises);
264278

279+
if(signal&&signal.aborted){
280+
throwlazyDOMException('The operation was aborted','AbortError');
281+
}
282+
265283
letsize;
266284
if((statFields[1/* mode */]&S_IFMT)===S_IFREG){
267285
size=statFields[8/* size */];
@@ -278,6 +296,9 @@ async function readFileHandle(filehandle, options){
278296
MathMin(size,kReadFileMaxChunkSize);
279297
letendOfFile=false;
280298
do{
299+
if(signal&&signal.aborted){
300+
throwlazyDOMException('The operation was aborted','AbortError');
301+
}
281302
constbuf=Buffer.alloc(chunkSize);
282303
const{ bytesRead, buffer }=
283304
awaitread(filehandle,buf,0,chunkSize,-1);

‎lib/internal/fs/read_file_context.js‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ const{Buffer } = require('buffer');
88

99
const{ FSReqCallback, close, read }=internalBinding('fs');
1010

11+
const{ hideStackFrames }=require('internal/errors');
12+
13+
14+
letDOMException;
15+
constlazyDOMException=hideStackFrames((message,name)=>{
16+
if(DOMException===undefined)
17+
DOMException=internalBinding('messaging').DOMException;
18+
returnnewDOMException(message,name);
19+
});
20+
1121
// Use 64kb in case the file type is not a regular file and thus do not know the
1222
// actual file size. Increasing the value further results in more frequent over
1323
// allocation for small files and consumes CPU time and memory that should be
@@ -74,13 +84,19 @@ class ReadFileContext{
7484
this.pos=0;
7585
this.encoding=encoding;
7686
this.err=null;
87+
this.signal=undefined;
7788
}
7889

7990
read(){
8091
letbuffer;
8192
letoffset;
8293
letlength;
8394

95+
if(this.signal&&this.signal.aborted){
96+
returnthis.close(
97+
lazyDOMException('The operation was aborted','AbortError')
98+
);
99+
}
84100
if(this.size===0){
85101
buffer=Buffer.allocUnsafeSlow(kReadFileUnknownBufferLength);
86102
offset=0;

‎lib/internal/fs/utils.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const{
3737
const{ once }=require('internal/util');
3838
const{ toPathIfFileURL }=require('internal/url');
3939
const{
40+
validateAbortSignal,
4041
validateBoolean,
4142
validateInt32,
4243
validateUint32
@@ -297,6 +298,10 @@ function getOptions(options, defaultOptions){
297298

298299
if(options.encoding!=='buffer')
299300
assertEncoding(options.encoding);
301+
302+
if(options.signal!==undefined){
303+
validateAbortSignal(options.signal,'options.signal');
304+
}
300305
returnoptions;
301306
}
302307

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

Lines changed: 38 additions & 12 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');
@@ -10,18 +11,21 @@ tmpdir.refresh();
1011

1112
constfn=path.join(tmpdir.path,'large-file');
1213

13-
asyncfunctionvalidateReadFile(){
14-
// Creating large buffer with random content
15-
constbuffer=Buffer.from(
16-
Array.apply(null,{length: 16834*2})
17-
.map(Math.random)
18-
.map((number)=>(number*(1<<8)))
19-
);
14+
// Creating large buffer with random content
15+
constlargeBuffer=Buffer.from(
16+
Array.apply(null,{length: 16834*2})
17+
.map(Math.random)
18+
.map((number)=>(number*(1<<8)))
19+
);
2020

21+
asyncfunctioncreateLargeFile(){
2122
// Writing buffer to a file then try to read it
22-
awaitwriteFile(fn,buffer);
23+
awaitwriteFile(fn,largeBuffer);
24+
}
25+
26+
asyncfunctionvalidateReadFile(){
2327
constreadBuffer=awaitreadFile(fn);
24-
assert.strictEqual(readBuffer.equals(buffer),true);
28+
assert.strictEqual(readBuffer.equals(largeBuffer),true);
2529
}
2630

2731
asyncfunctionvalidateReadFileProc(){
@@ -39,6 +43,28 @@ async function validateReadFileProc(){
3943
assert.ok(hostname.length>0);
4044
}
4145

42-
validateReadFile()
43-
.then(()=>validateReadFileProc())
44-
.then(common.mustCall());
46+
functionvalidateReadFileAbortLogicBefore(){
47+
constcontroller=newAbortController();
48+
constsignal=controller.signal;
49+
controller.abort();
50+
assert.rejects(readFile(fn,{ signal }),{
51+
name: 'AbortError'
52+
});
53+
}
54+
55+
functionvalidateReadFileAbortLogicDuring(){
56+
constcontroller=newAbortController();
57+
constsignal=controller.signal;
58+
process.nextTick(()=>controller.abort());
59+
assert.rejects(readFile(fn,{ signal }),{
60+
name: 'AbortError'
61+
});
62+
}
63+
64+
(async()=>{
65+
awaitcreateLargeFile();
66+
awaitvalidateReadFile();
67+
awaitvalidateReadFileProc();
68+
awaitvalidateReadFileAbortLogicBefore();
69+
awaitvalidateReadFileAbortLogicDuring();
70+
})().then(common.mustCall());

‎test/parallel/test-fs-readfile.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --experimental-abortcontroller
12
'use strict';
23
constcommon=require('../common');
34

@@ -57,3 +58,21 @@ for (const e of fileInfo){
5758
assert.deepStrictEqual(buf,e.contents);
5859
}));
5960
}
61+
{
62+
// Test cancellation, before
63+
constcontroller=newAbortController();
64+
constsignal=controller.signal;
65+
controller.abort();
66+
fs.readFile(fileInfo[0].name,{ signal },common.mustCall((err,buf)=>{
67+
assert.strictEqual(err.name,'AbortError');
68+
}));
69+
}
70+
{
71+
// Test cancellation, during read
72+
constcontroller=newAbortController();
73+
constsignal=controller.signal;
74+
fs.readFile(fileInfo[0].name,{ signal },common.mustCall((err,buf)=>{
75+
assert.strictEqual(err.name,'AbortError');
76+
}));
77+
process.nextTick(()=>controller.abort());
78+
}

0 commit comments

Comments
(0)