Skip to content

Commit b1991f2

Browse files
benjamingrtargos
authored andcommitted
fs: add support for AbortSignal in readFile
PR-URL: nodejs#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 86db4e3 commit b1991f2

File tree

7 files changed

+146
-14
lines changed

7 files changed

+146
-14
lines changed

doc/api/fs.md

+42
Original file line numberDiff line numberDiff line change
@@ -3019,6 +3019,10 @@ If `options.withFileTypes` is set to `true`, the result will contain
30193019
<!-- YAML
30203020
added: v0.1.29
30213021
changes:
3022+
- version: REPLACEME
3023+
pr-url: https://github.com/nodejs/node/pull/35911
3024+
description: The options argument may include an AbortSignal to abort an
3025+
ongoing readFile request.
30223026
- version: v10.0.0
30233027
pr-url: https://github.com/nodejs/node/pull/12562
30243028
description: The `callback` parameter is no longer optional. Not passing
@@ -3044,6 +3048,7 @@ changes:
30443048
* `options` {Object|string}
30453049
* `encoding` {string|null} **Default:** `null`
30463050
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
3051+
* `signal` {AbortSignal} allows aborting an in-progress readFile
30473052
* `callback` {Function}
30483053
* `err` {Error}
30493054
* `data` {string|Buffer}
@@ -3085,9 +3090,25 @@ fs.readFile('<directory>', (err, data) => {
30853090
});
30863091
```
30873092

3093+
It is possible to abort an ongoing request using an `AbortSignal`. If a
3094+
request is aborted the callback is called with an `AbortError`:
3095+
3096+
```js
3097+
const controller = new AbortController();
3098+
const signal = controller.signal;
3099+
fs.readFile(fileInfo[0].name, { signal }, (err, buf) => {
3100+
// ...
3101+
});
3102+
// When you want to abort the request
3103+
controller.abort();
3104+
```
3105+
30883106
The `fs.readFile()` function buffers the entire file. To minimize memory costs,
30893107
when possible prefer streaming via `fs.createReadStream()`.
30903108

3109+
Aborting an ongoing request does not abort individual operating
3110+
system requests but rather the internal buffering `fs.readFile` performs.
3111+
30913112
### File descriptors
30923113

30933114
1. Any specified file descriptor has to support reading.
@@ -4734,6 +4755,7 @@ added: v10.0.0
47344755

47354756
* `options` {Object|string}
47364757
* `encoding` {string|null} **Default:** `null`
4758+
* `signal` {AbortSignal} allows aborting an in-progress readFile
47374759
* Returns: {Promise}
47384760

47394761
Asynchronously reads the entire contents of a file.
@@ -5397,12 +5419,18 @@ print('./').catch(console.error);
53975419
### `fsPromises.readFile(path[, options])`
53985420
<!-- YAML
53995421
added: v10.0.0
5422+
changes:
5423+
- version: REPLACEME
5424+
pr-url: https://github.com/nodejs/node/pull/35911
5425+
description: The options argument may include an AbortSignal to abort an
5426+
ongoing readFile request.
54005427
-->
54015428

54025429
* `path` {string|Buffer|URL|FileHandle} filename or `FileHandle`
54035430
* `options` {Object|string}
54045431
* `encoding` {string|null} **Default:** `null`
54055432
* `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`.
5433+
* `signal` {AbortSignal} allows aborting an in-progress readFile
54065434
* Returns: {Promise}
54075435

54085436
Asynchronously reads the entire contents of a file.
@@ -5418,6 +5446,20 @@ platform-specific. On macOS, Linux, and Windows, the promise will be rejected
54185446
with an error. On FreeBSD, a representation of the directory's contents will be
54195447
returned.
54205448

5449+
It is possible to abort an ongoing `readFile` using an `AbortSignal`. If a
5450+
request is aborted the promise returned is rejected with an `AbortError`:
5451+
5452+
```js
5453+
const controller = new AbortController();
5454+
const signal = controller.signal;
5455+
readFile(fileName, { signal }).then((file) => { /* ... */ });
5456+
// Abort the request
5457+
controller.abort();
5458+
```
5459+
5460+
Aborting an ongoing request does not abort individual operating
5461+
system requests but rather the internal buffering `fs.readFile` performs.
5462+
54215463
Any specified `FileHandle` has to support reading.
54225464

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

lib/fs.js

+3
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ function readFile(path, options, callback) {
315315
const context = new ReadFileContext(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(function tick(context) {
320323
readFileAfterOpen.call({ context }, null, path);

lib/internal/fs/promises.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ const {
3030
} = internalBinding('constants').fs;
3131
const binding = 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
const getDirectoryEntriesPromise = promisify(getDirents);
8486
const validateRmOptionsPromise = promisify(validateRmOptions);
8587

88+
let DOMException;
89+
const lazyDOMException = hideStackFrames((message, name) => {
90+
if (DOMException === undefined)
91+
DOMException = internalBinding('messaging').DOMException;
92+
return new DOMException(message, name);
93+
});
94+
8695
class FileHandle extends JSTransferable {
8796
constructor(filehandle) {
8897
super();
@@ -260,8 +269,17 @@ async function writeFileHandle(filehandle, data) {
260269
}
261270

262271
async function readFileHandle(filehandle, options) {
272+
const signal = options && options.signal;
273+
274+
if (signal && signal.aborted) {
275+
throw lazyDOMException('The operation was aborted', 'AbortError');
276+
}
263277
const statFields = await binding.fstat(filehandle.fd, false, kUsePromises);
264278

279+
if (signal && signal.aborted) {
280+
throw lazyDOMException('The operation was aborted', 'AbortError');
281+
}
282+
265283
let size;
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
let endOfFile = false;
280298
do {
299+
if (signal && signal.aborted) {
300+
throw lazyDOMException('The operation was aborted', 'AbortError');
301+
}
281302
const buf = Buffer.alloc(chunkSize);
282303
const { bytesRead, buffer } =
283304
await read(filehandle, buf, 0, chunkSize, -1);

lib/internal/fs/read_file_context.js

+16
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+
let DOMException;
15+
const lazyDOMException = hideStackFrames((message, name) => {
16+
if (DOMException === undefined)
17+
DOMException = internalBinding('messaging').DOMException;
18+
return new DOMException(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
let buffer;
8192
let offset;
8293
let length;
8394

95+
if (this.signal && this.signal.aborted) {
96+
return this.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

+5
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
return options;
301306
}
302307

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

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

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

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

21+
async function createLargeFile() {
2122
// Writing buffer to a file then try to read it
22-
await writeFile(fn, buffer);
23+
await writeFile(fn, largeBuffer);
24+
}
25+
26+
async function validateReadFile() {
2327
const readBuffer = await readFile(fn);
24-
assert.strictEqual(readBuffer.equals(buffer), true);
28+
assert.strictEqual(readBuffer.equals(largeBuffer), true);
2529
}
2630

2731
async function validateReadFileProc() {
@@ -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+
function validateReadFileAbortLogicBefore() {
47+
const controller = new AbortController();
48+
const signal = controller.signal;
49+
controller.abort();
50+
assert.rejects(readFile(fn, { signal }), {
51+
name: 'AbortError'
52+
});
53+
}
54+
55+
function validateReadFileAbortLogicDuring() {
56+
const controller = new AbortController();
57+
const signal = controller.signal;
58+
process.nextTick(() => controller.abort());
59+
assert.rejects(readFile(fn, { signal }), {
60+
name: 'AbortError'
61+
});
62+
}
63+
64+
(async () => {
65+
await createLargeFile();
66+
await validateReadFile();
67+
await validateReadFileProc();
68+
await validateReadFileAbortLogicBefore();
69+
await validateReadFileAbortLogicDuring();
70+
})().then(common.mustCall());

test/parallel/test-fs-readfile.js

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --experimental-abortcontroller
12
'use strict';
23
const common = 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+
const controller = new AbortController();
64+
const signal = 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+
const controller = new AbortController();
73+
const signal = 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)