Skip to content

Commit 92bdfd1

Browse files
benjamingrdanielleadams
authored andcommitted
fs: add support for AbortSignal in readFile
PR-URL: #35911 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
1 parent d83e253 commit 92bdfd1

7 files changed

Lines changed: 144 additions & 14 deletions

File tree

‎doc/api/fs.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,10 @@ If `options.withFileTypes` is set to `true`, the result will contain
30313031
<!-- YAML
30323032
added: v0.1.29
30333033
changes:
3034+
- version: REPLACEME
3035+
pr-url: https://github.com/nodejs/node/pull/35911
3036+
description: The options argument may include an AbortSignal to abort an
3037+
ongoing readFile request.
30343038
- version: v10.0.0
30353039
pr-url: https://github.com/nodejs/node/pull/12562
30363040
description: The `callback` parameter is no longer optional. Not passing
@@ -3056,6 +3060,7 @@ changes:
30563060
*`options` {Object|string}
30573061
*`encoding` {string|null} **Default:**`null`
30583062
*`flag` {string} See [support of file system `flags`][]. **Default:**`'r'`.
3063+
*`signal` {AbortSignal} allows aborting an in-progress readFile
30593064
*`callback` {Function}
30603065
*`err` {Error}
30613066
*`data` {string|Buffer}
@@ -3097,9 +3102,25 @@ fs.readFile('<directory>', (err, data) => {
30973102
});
30983103
```
30993104

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

3121+
Aborting an ongoing request does not abort individual operating
3122+
system requests but rather the internal buffering `fs.readFile` performs.
3123+
31033124
### File descriptors
31043125

31053126
1. Any specified file descriptor has to support reading.
@@ -4771,6 +4792,7 @@ added: v10.0.0
47714792

47724793
*`options` {Object|string}
47734794
*`encoding` {string|null} **Default:**`null`
4795+
*`signal` {AbortSignal} allows aborting an in-progress readFile
47744796
* Returns: {Promise}
47754797

47764798
Asynchronously reads the entire contents of a file.
@@ -5438,12 +5460,18 @@ print('./').catch(console.error);
54385460
### `fsPromises.readFile(path[, options])`
54395461
<!-- YAML
54405462
added: v10.0.0
5463+
changes:
5464+
- version: REPLACEME
5465+
pr-url: https://github.com/nodejs/node/pull/35911
5466+
description: The options argument may include an AbortSignal to abort an
5467+
ongoing readFile request.
54415468
-->
54425469

54435470
*`path` {string|Buffer|URL|FileHandle} filename or `FileHandle`
54445471
*`options` {Object|string}
54455472
*`encoding` {string|null} **Default:**`null`
54465473
*`flag` {string} See [support of file system `flags`][]. **Default:**`'r'`.
5474+
*`signal` {AbortSignal} allows aborting an in-progress readFile
54475475
* Returns: {Promise}
54485476

54495477
Asynchronously reads the entire contents of a file.
@@ -5459,6 +5487,20 @@ platform-specific. On macOS, Linux, and Windows, the promise will be rejected
54595487
with an error. On FreeBSD, a representation of the directory's contents will be
54605488
returned.
54615489

5490+
It is possible to abort an ongoing `readFile` using an `AbortSignal`. If a
5491+
request is aborted the promise returned is rejected with an `AbortError`:
5492+
5493+
```js
5494+
constcontroller=newAbortController();
5495+
constsignal=controller.signal;
5496+
readFile(fileName, { signal }).then((file) => { /* ... */ });
5497+
// Abort the request
5498+
controller.abort();
5499+
```
5500+
5501+
Aborting an ongoing request does not abort individual operating
5502+
system requests but rather the internal buffering `fs.readFile` performs.
5503+
54625504
Any specified `FileHandle` has to support reading.
54635505

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

‎lib/fs.js‎

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

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

‎lib/internal/fs/promises.js‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ const {
2929
}=internalBinding('constants').fs;
3030
constbinding=internalBinding('fs');
3131
const{ Buffer }=require('buffer');
32+
33+
const{ codes, hideStackFrames }=require('internal/errors');
3234
const{
3335
ERR_FS_FILE_TOO_LARGE,
3436
ERR_INVALID_ARG_TYPE,
3537
ERR_INVALID_ARG_VALUE,
36-
ERR_METHOD_NOT_IMPLEMENTED
37-
}=require('internal/errors').codes;
38+
ERR_METHOD_NOT_IMPLEMENTED,
39+
}=codes;
3840
const{ isArrayBufferView }=require('internal/util/types');
3941
const{ rimrafPromises }=require('internal/fs/rimraf');
4042
const{
@@ -82,6 +84,13 @@ const {
8284
constgetDirectoryEntriesPromise=promisify(getDirents);
8385
constvalidateRmOptionsPromise=promisify(validateRmOptions);
8486

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

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

278+
if(signal&&signal.aborted){
279+
throwlazyDOMException('The operation was aborted','AbortError');
280+
}
281+
264282
letsize;
265283
if((statFields[1/* mode */]&S_IFMT)===S_IFREG){
266284
size=statFields[8/* size */];
@@ -277,6 +295,9 @@ async function readFileHandle(filehandle, options) {
277295
MathMin(size,kReadFileMaxChunkSize);
278296
letendOfFile=false;
279297
do{
298+
if(signal&&signal.aborted){
299+
throwlazyDOMException('The operation was aborted','AbortError');
300+
}
280301
constbuf=Buffer.alloc(chunkSize);
281302
const{ bytesRead, buffer }=
282303
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
@@ -35,6 +35,7 @@ const {
3535
const{ once }=require('internal/util');
3636
const{ toPathIfFileURL }=require('internal/url');
3737
const{
38+
validateAbortSignal,
3839
validateBoolean,
3940
validateInt32,
4041
validateUint32
@@ -296,6 +297,10 @@ function getOptions(options, defaultOptions) {
296297

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

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

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,21 @@ tmpdir.refresh();
1010

1111
constfn=path.join(tmpdir.path,'large-file');
1212

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

20+
asyncfunctioncreateLargeFile(){
2121
// Writing buffer to a file then try to read it
22-
awaitwriteFile(fn,buffer);
22+
awaitwriteFile(fn,largeBuffer);
23+
}
24+
25+
asyncfunctionvalidateReadFile(){
2326
constreadBuffer=awaitreadFile(fn);
24-
assert.strictEqual(readBuffer.equals(buffer),true);
27+
assert.strictEqual(readBuffer.equals(largeBuffer),true);
2528
}
2629

2730
asyncfunctionvalidateReadFileProc(){
@@ -39,6 +42,28 @@ async function validateReadFileProc() {
3942
assert.ok(hostname.length>0);
4043
}
4144

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

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,21 @@ for (const e of fileInfo) {
5757
assert.deepStrictEqual(buf,e.contents);
5858
}));
5959
}
60+
{
61+
// Test cancellation, before
62+
constcontroller=newAbortController();
63+
constsignal=controller.signal;
64+
controller.abort();
65+
fs.readFile(fileInfo[0].name,{ signal },common.mustCall((err,buf)=>{
66+
assert.strictEqual(err.name,'AbortError');
67+
}));
68+
}
69+
{
70+
// Test cancellation, during read
71+
constcontroller=newAbortController();
72+
constsignal=controller.signal;
73+
fs.readFile(fileInfo[0].name,{ signal },common.mustCall((err,buf)=>{
74+
assert.strictEqual(err.name,'AbortError');
75+
}));
76+
process.nextTick(()=>controller.abort());
77+
}

0 commit comments

Comments
 (0)