diff --git a/doc/api/fs.md b/doc/api/fs.md index e9cba0cceedbf5..d6d95dd149f9f8 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -3026,6 +3026,10 @@ If `options.withFileTypes` is set to `true`, the result will contain * `path` {string|Buffer|URL|FileHandle} filename or `FileHandle` * `options` {Object|string} * `encoding` {string|null} **Default:** `null` * `flag` {string} See [support of file system `flags`][]. **Default:** `'r'`. + * `signal` {AbortSignal} allows aborting an in-progress readFile * Returns: {Promise} Asynchronously reads the entire contents of a file. @@ -5432,6 +5460,20 @@ platform-specific. On macOS, Linux, and Windows, the promise will be rejected with an error. On FreeBSD, a representation of the directory's contents will be returned. +It is possible to abort an ongoing `readFile` using an `AbortSignal`. If a +request is aborted the promise returned is rejected with an `AbortError`: + +```js +const controller = new AbortController(); +const signal = controller.signal; +readFile(fileName, { signal }).then((file) => { /* ... */ }); +// Abort the request +controller.abort(); +``` + +Aborting an ongoing request does not abort individual operating +system requests but rather the internal buffering `fs.readFile` performs. + Any specified `FileHandle` has to support reading. ### `fsPromises.readlink(path[, options])` diff --git a/lib/fs.js b/lib/fs.js index 5746fbe1349c74..3aa7a901a562a2 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -316,6 +316,9 @@ function readFile(path, options, callback) { const context = new ReadFileContext(callback, options.encoding); context.isUserFd = isFd(path); // File descriptor ownership + if (options.signal) { + context.signal = options.signal; + } if (context.isUserFd) { process.nextTick(function tick(context) { readFileAfterOpen.call({ context }, null, path); diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 1f81a6f47b1d59..d4e83947feb193 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -30,12 +30,14 @@ const { } = internalBinding('constants').fs; const binding = internalBinding('fs'); const { Buffer } = require('buffer'); + +const { codes, hideStackFrames } = require('internal/errors'); const { ERR_FS_FILE_TOO_LARGE, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, - ERR_METHOD_NOT_IMPLEMENTED -} = require('internal/errors').codes; + ERR_METHOD_NOT_IMPLEMENTED, +} = codes; const { isArrayBufferView } = require('internal/util/types'); const { rimrafPromises } = require('internal/fs/rimraf'); const { @@ -83,6 +85,13 @@ const { const getDirectoryEntriesPromise = promisify(getDirents); const validateRmOptionsPromise = promisify(validateRmOptions); +let DOMException; +const lazyDOMException = hideStackFrames((message, name) => { + if (DOMException === undefined) + DOMException = internalBinding('messaging').DOMException; + return new DOMException(message, name); +}); + class FileHandle extends JSTransferable { constructor(filehandle) { super(); @@ -260,8 +269,17 @@ async function writeFileHandle(filehandle, data) { } async function readFileHandle(filehandle, options) { + const signal = options && options.signal; + + if (signal && signal.aborted) { + throw lazyDOMException('The operation was aborted', 'AbortError'); + } const statFields = await binding.fstat(filehandle.fd, false, kUsePromises); + if (signal && signal.aborted) { + throw lazyDOMException('The operation was aborted', 'AbortError'); + } + let size; if ((statFields[1/* mode */] & S_IFMT) === S_IFREG) { size = statFields[8/* size */]; @@ -278,6 +296,9 @@ async function readFileHandle(filehandle, options) { MathMin(size, kReadFileMaxChunkSize); let endOfFile = false; do { + if (signal && signal.aborted) { + throw lazyDOMException('The operation was aborted', 'AbortError'); + } const buf = Buffer.alloc(chunkSize); const { bytesRead, buffer } = await read(filehandle, buf, 0, chunkSize, -1); diff --git a/lib/internal/fs/read_file_context.js b/lib/internal/fs/read_file_context.js index 5091a34231665b..faca0e0c331e39 100644 --- a/lib/internal/fs/read_file_context.js +++ b/lib/internal/fs/read_file_context.js @@ -8,6 +8,16 @@ const { Buffer } = require('buffer'); const { FSReqCallback, close, read } = internalBinding('fs'); +const { hideStackFrames } = require('internal/errors'); + + +let DOMException; +const lazyDOMException = hideStackFrames((message, name) => { + if (DOMException === undefined) + DOMException = internalBinding('messaging').DOMException; + return new DOMException(message, name); +}); + // Use 64kb in case the file type is not a regular file and thus do not know the // actual file size. Increasing the value further results in more frequent over // allocation for small files and consumes CPU time and memory that should be @@ -74,6 +84,7 @@ class ReadFileContext { this.pos = 0; this.encoding = encoding; this.err = null; + this.signal = undefined; } read() { @@ -81,6 +92,11 @@ class ReadFileContext { let offset; let length; + if (this.signal && this.signal.aborted) { + return this.close( + lazyDOMException('The operation was aborted', 'AbortError') + ); + } if (this.size === 0) { buffer = Buffer.allocUnsafeSlow(kReadFileUnknownBufferLength); offset = 0; diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index e3255cdb2f2562..3ff3d9c561675c 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -37,6 +37,7 @@ const { const { once } = require('internal/util'); const { toPathIfFileURL } = require('internal/url'); const { + validateAbortSignal, validateBoolean, validateInt32, validateUint32 @@ -297,6 +298,10 @@ function getOptions(options, defaultOptions) { if (options.encoding !== 'buffer') assertEncoding(options.encoding); + + if (options.signal !== undefined) { + validateAbortSignal(options.signal, 'options.signal'); + } return options; } diff --git a/test/parallel/test-fs-promises-readfile.js b/test/parallel/test-fs-promises-readfile.js index ff25be75b84ff0..10632ba62e770b 100644 --- a/test/parallel/test-fs-promises-readfile.js +++ b/test/parallel/test-fs-promises-readfile.js @@ -1,3 +1,4 @@ +// Flags: --experimental-abortcontroller 'use strict'; const common = require('../common'); @@ -10,18 +11,21 @@ tmpdir.refresh(); const fn = path.join(tmpdir.path, 'large-file'); -async function validateReadFile() { - // Creating large buffer with random content - const buffer = Buffer.from( - Array.apply(null, { length: 16834 * 2 }) - .map(Math.random) - .map((number) => (number * (1 << 8))) - ); +// Creating large buffer with random content +const largeBuffer = Buffer.from( + Array.apply(null, { length: 16834 * 2 }) + .map(Math.random) + .map((number) => (number * (1 << 8))) +); +async function createLargeFile() { // Writing buffer to a file then try to read it - await writeFile(fn, buffer); + await writeFile(fn, largeBuffer); +} + +async function validateReadFile() { const readBuffer = await readFile(fn); - assert.strictEqual(readBuffer.equals(buffer), true); + assert.strictEqual(readBuffer.equals(largeBuffer), true); } async function validateReadFileProc() { @@ -39,6 +43,28 @@ async function validateReadFileProc() { assert.ok(hostname.length > 0); } -validateReadFile() - .then(() => validateReadFileProc()) - .then(common.mustCall()); +function validateReadFileAbortLogicBefore() { + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + assert.rejects(readFile(fn, { signal }), { + name: 'AbortError' + }); +} + +function validateReadFileAbortLogicDuring() { + const controller = new AbortController(); + const signal = controller.signal; + process.nextTick(() => controller.abort()); + assert.rejects(readFile(fn, { signal }), { + name: 'AbortError' + }); +} + +(async () => { + await createLargeFile(); + await validateReadFile(); + await validateReadFileProc(); + await validateReadFileAbortLogicBefore(); + await validateReadFileAbortLogicDuring(); +})().then(common.mustCall()); diff --git a/test/parallel/test-fs-readfile.js b/test/parallel/test-fs-readfile.js index 648bf692d1dcc8..b5cfabc547ad82 100644 --- a/test/parallel/test-fs-readfile.js +++ b/test/parallel/test-fs-readfile.js @@ -1,3 +1,4 @@ +// Flags: --experimental-abortcontroller 'use strict'; const common = require('../common'); @@ -57,3 +58,21 @@ for (const e of fileInfo) { assert.deepStrictEqual(buf, e.contents); })); } +{ + // Test cancellation, before + const controller = new AbortController(); + const signal = controller.signal; + controller.abort(); + fs.readFile(fileInfo[0].name, { signal }, common.mustCall((err, buf) => { + assert.strictEqual(err.name, 'AbortError'); + })); +} +{ + // Test cancellation, during read + const controller = new AbortController(); + const signal = controller.signal; + fs.readFile(fileInfo[0].name, { signal }, common.mustCall((err, buf) => { + assert.strictEqual(err.name, 'AbortError'); + })); + process.nextTick(() => controller.abort()); +}