Skip to content
24 changes: 24 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,10 @@ link(2) documentation for more detail.
<!-- YAML
added: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63143
description: Accepts an additional `signal` option to allow aborting the
operation.
- version: v10.5.0
pr-url: https://github.com/nodejs/node/pull/20220
description: Accepts an additional `options` object to specify whether
Expand All @@ -1554,6 +1558,8 @@ changes:
* `options` {Object}
* `bigint` {boolean} Whether the numeric values in the returned
{fs.Stats} object should be `bigint`. **Default:** `false`.
* `signal` {AbortSignal} An AbortSignal to cancel the operation.
**Default:** `undefined`.
* Returns: {Promise} Fulfills with the {fs.Stats} object for the given
symbolic link `path`.

Expand Down Expand Up @@ -2084,6 +2090,10 @@ Removes files and directories (modeled on the standard POSIX `rm` utility).
<!-- YAML
added: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63143
description: Accepts an additional `signal` option to allow aborting the
operation.
- version: v25.7.0
pr-url: https://github.com/nodejs/node/pull/61178
description: Accepts a `throwIfNoEntry` option to specify whether
Expand All @@ -2101,6 +2111,8 @@ changes:
* `throwIfNoEntry` {boolean} Whether an exception will be thrown
if no file system entry exists, rather than returning `undefined`.
**Default:** `true`.
* `signal` {AbortSignal} An AbortSignal to cancel the operation.
**Default:** `undefined`.
* Returns: {Promise} Fulfills with the {fs.Stats} object for the
given `path`.

Expand Down Expand Up @@ -3437,6 +3449,10 @@ exception are given to the completion callback.
<!-- YAML
added: v0.1.95
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63143
description: Accepts an additional `signal` option to allow aborting the
operation.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand All @@ -3460,6 +3476,8 @@ changes:
* `options` {Object}
* `bigint` {boolean} Whether the numeric values in the returned
{fs.Stats} object should be `bigint`. **Default:** `false`.
* `signal` {AbortSignal} An AbortSignal to cancel the operation.
**Default:** `undefined`.
* `callback` {Function}
* `err` {Error}
* `stats` {fs.Stats}
Expand Down Expand Up @@ -3803,6 +3821,10 @@ exception are given to the completion callback.
<!-- YAML
added: v0.1.30
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63143
description: Accepts an additional `signal` option to allow aborting the
operation.
- version: v18.0.0
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand Down Expand Up @@ -3830,6 +3852,8 @@ changes:
* `options` {Object}
* `bigint` {boolean} Whether the numeric values in the returned
{fs.Stats} object should be `bigint`. **Default:** `false`.
* `signal` {AbortSignal} An AbortSignal to cancel the operation.
**Default:** `undefined`.
* `callback` {Function}
* `err` {Error}
* `stats` {fs.Stats}
Expand Down
45 changes: 39 additions & 6 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const {
const {
isInt32,
parseFileMode,
validateAbortSignal,
validateBoolean,
validateBuffer,
validateEncoding,
Expand Down Expand Up @@ -383,6 +384,25 @@ function checkAborted(signal, callback) {
return false;
}

function bindSignalToReq(req, signal, callback) {
if (!signal) {
req.oncomplete = callback;
return;
}
let aborted = false;
const onAbort = () => {
aborted = true;
callback(new AbortError(undefined, { cause: signal.reason }));
};
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
signal.addEventListener('abort', onAbort, { __proto__: null, [kResistStopPropagation]: true });
req.oncomplete = function(err, result) {
signal.removeEventListener('abort', onAbort);
if (aborted) return;
callback(err, result);
};
}

/**
* Asynchronously reads the entire contents of a file.
* @param {string | Buffer | URL | number} path
Expand Down Expand Up @@ -1893,7 +1913,7 @@ function readdirSync(path, options) {
* Invokes the callback with the `fs.Stats`
* for the file descriptor.
* @param {number} fd
* @param {{ bigint?: boolean; }} [options]
* @param {{ bigint?: boolean, signal?: AbortSignal }} [options]
* @param {(
* err?: Error,
* stats?: Stats
Expand All @@ -1904,23 +1924,28 @@ function fstat(fd, options = { __proto__: null, bigint: false }, callback) {
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
} else if (options === null || typeof options !== 'object') {
options = kEmptyObject;
}

const h = vfsState.handlers;
if (h !== null && vfsResult(h.fstat(fd, options), callback)) return;

callback = makeStatsCallback(callback);

if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal');
if (checkAborted(options.signal, callback)) return;

const req = new FSReqCallback(options.bigint);
req.oncomplete = callback;
bindSignalToReq(req, options.signal, callback);
binding.fstat(fd, options.bigint, req);
}

/**
* Retrieves the `fs.Stats` for the symbolic link
* referred to by the `path`.
* @param {string | Buffer | URL} path
* @param {{ bigint?: boolean; }} [options]
* @param {{ bigint?: boolean, signal?: AbortSignal }} [options]
* @param {(
* err?: Error,
* stats?: Stats
Expand All @@ -1931,6 +1956,10 @@ function lstat(path, options = { __proto__: null, bigint: false }, callback) {
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
} else if (options === null || typeof options !== 'object') {
options = kEmptyObject;
} else {
options = getOptions(options, { bigint: false });
}

const h = vfsState.handlers;
Expand All @@ -1944,8 +1973,11 @@ function lstat(path, options = { __proto__: null, bigint: false }, callback) {
return;
}

if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal');
if (checkAborted(options.signal, callback)) return;

const req = new FSReqCallback(options.bigint);
req.oncomplete = callback;
bindSignalToReq(req, options.signal, callback);
binding.lstat(path, options.bigint, req);
}

Expand Down Expand Up @@ -1975,11 +2007,12 @@ function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry:
callback = makeStatsCallback(callback);
path = getValidatedPath(path);

if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal');
if (checkAborted(options.signal, callback)) return;

const req = new FSReqCallback(options.bigint);
req.oncomplete = callback;
binding.stat(getValidatedPath(path), options.bigint, req, options.throwIfNoEntry);
bindSignalToReq(req, options.signal, callback);
binding.stat(path, options.bigint, req, options.throwIfNoEntry);
}

function statfs(path, options = { __proto__: null, bigint: false }, callback) {
Expand Down
62 changes: 50 additions & 12 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ const {
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
SafeArrayIterator,
SafePromisePrototypeFinally,
SafePromiseRace,
Symbol,
SymbolAsyncDispose,
SymbolAsyncIterator,
Expand Down Expand Up @@ -1126,6 +1128,21 @@ function checkAborted(signal) {
throw new AbortError(undefined, { cause: signal.reason });
}

function rejectWithReason(signal, reject) {
return () => {
reject(new AbortError(undefined, { cause: signal.reason }));
};
}

async function raceWithSignal(opPromise, signal) {
if (!signal) return opPromise;
const { promise: abortPromise, reject } = PromiseWithResolvers();
// eslint-disable-next-line no-unused-vars
using _ = EventEmitter.addAbortListener(signal,
rejectWithReason(signal, reject));
return await SafePromiseRace([opPromise, abortPromise]);
}
Comment thread
mertcanaltin marked this conversation as resolved.

async function writeFileHandle(filehandle, data, signal, encoding) {
checkAborted(signal);
if (isCustomIterable(data)) {
Expand Down Expand Up @@ -1773,15 +1790,26 @@ async function symlink(target, path, type) {
}

async function fstat(handle, options = { __proto__: null, bigint: false }) {
const result = await PromisePrototypeThen(
binding.fstat(handle.fd, options.bigint, kUsePromises),
undefined,
handleErrorFromBinding,
validateObject(options, 'options');
const { signal } = options;
if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
Comment thread
mertcanaltin marked this conversation as resolved.
checkAborted(signal);
const result = await raceWithSignal(
PromisePrototypeThen(
binding.fstat(handle.fd, options.bigint, kUsePromises),
undefined,
handleErrorFromBinding,
),
signal,
);
return getStatsFromBinding(result);
}

async function lstat(path, options = { __proto__: null, bigint: false }) {
validateObject(options, 'options');
const { signal } = options;
if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
Comment thread
mertcanaltin marked this conversation as resolved.
checkAborted(signal);
const h = vfsState.handlers;
if (h !== null) {
const promise = h.lstat(path, options);
Expand All @@ -1792,24 +1820,34 @@ async function lstat(path, options = { __proto__: null, bigint: false }) {
const resource = pathModule.toNamespacedPath(BufferIsBuffer(path) ? BufferToString(path) : path);
throw new ERR_ACCESS_DENIED('Access to this API has been restricted', 'FileSystemRead', resource);
}
const result = await PromisePrototypeThen(
binding.lstat(path, options.bigint, kUsePromises),
undefined,
handleErrorFromBinding,
const result = await raceWithSignal(
PromisePrototypeThen(
binding.lstat(path, options.bigint, kUsePromises),
undefined,
handleErrorFromBinding,
),
signal,
);
return getStatsFromBinding(result);
}

async function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) {
validateObject(options, 'options');
const { signal } = options;
if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
Comment thread
mertcanaltin marked this conversation as resolved.
checkAborted(signal);
const h = vfsState.handlers;
if (h !== null) {
const promise = h.stat(path, options);
if (promise !== undefined) return await promise;
}
const result = await PromisePrototypeThen(
binding.stat(getValidatedPath(path), options.bigint, kUsePromises, options.throwIfNoEntry),
undefined,
handleErrorFromBinding,
const result = await raceWithSignal(
PromisePrototypeThen(
binding.stat(getValidatedPath(path), options.bigint, kUsePromises, options.throwIfNoEntry),
undefined,
handleErrorFromBinding,
),
signal,
);

// Binding will resolve undefined if UV_ENOENT or UV_ENOTDIR and throwIfNoEntry is false
Expand Down
Loading
Loading