Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1478,16 +1478,22 @@
<!-- YAML
added: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/57775
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
the numeric values returned should be bigint.

Check warning on line 1488 in doc/api/fs.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
-->

* `path` {string|Buffer|URL}
* `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 @@ -1982,10 +1988,14 @@
<!-- YAML
added: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/57775
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
an exception should be thrown if the entry does not exist.

Check warning on line 1998 in doc/api/fs.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
- version: v10.5.0
pr-url: https://github.com/nodejs/node/pull/20220
description: Accepts an additional `options` object to specify whether
Expand All @@ -1999,6 +2009,8 @@
* `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 @@ -3309,10 +3321,14 @@
<!-- YAML
added: v0.1.95
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/57775
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
now throws `ERR_INVALID_ARG_TYPE` instead of

Check warning on line 3331 in doc/api/fs.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
`ERR_INVALID_CALLBACK`.
- version: v10.5.0
pr-url: https://github.com/nodejs/node/pull/20220
Expand All @@ -3332,6 +3348,8 @@
* `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 @@ -3673,10 +3691,14 @@
<!-- YAML
added: v0.1.30
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/57775
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
now throws `ERR_INVALID_ARG_TYPE` instead of

Check warning on line 3701 in doc/api/fs.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
`ERR_INVALID_CALLBACK`.
- version: v10.5.0
pr-url: https://github.com/nodejs/node/pull/20220
Expand All @@ -3700,6 +3722,8 @@
* `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 @@ -136,6 +136,7 @@ const {
const {
isInt32,
parseFileMode,
validateAbortSignal,
validateBoolean,
validateBuffer,
validateEncoding,
Expand Down Expand Up @@ -340,6 +341,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 @@ -1564,7 +1584,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 @@ -1575,19 +1595,24 @@ function fstat(fd, options = { bigint: false }, callback) {
if (typeof options === 'function') {
callback = options;
options = kEmptyObject;
} else if (options === null || typeof options !== 'object') {
options = kEmptyObject;
}
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 @@ -1598,6 +1623,10 @@ function lstat(path, options = { 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 });
}
callback = makeStatsCallback(callback);
path = getValidatedPath(path);
Expand All @@ -1607,8 +1636,11 @@ function lstat(path, options = { 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 All @@ -1635,11 +1667,12 @@ function stat(path, options = { bigint: false, throwIfNoEntry: true }, callback)
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 = { bigint: false }, callback) {
Expand Down
63 changes: 51 additions & 12 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
PromiseResolve,
SafeArrayIterator,
SafePromisePrototypeFinally,
SafePromiseRace,
Symbol,
SymbolAsyncDispose,
SymbolAsyncIterator,
Expand Down Expand Up @@ -125,6 +126,7 @@ const kLocked = Symbol('kLocked');
const kCloseSync = Symbol('kCloseSync');

const { kUsePromises } = binding;
let kResistStopPropagation;
const { Interface } = require('internal/readline/interface');
const {
kDeserialize, kTransfer, kTransferList, markTransferMode,
Expand Down Expand Up @@ -1116,6 +1118,25 @@ function checkAborted(signal) {
throw new AbortError(undefined, { cause: signal.reason });
}

async function raceWithSignal(opPromise, signal) {
if (!signal) return opPromise;
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
let onAbort;
const abortPromise = new Promise((_, reject) => {
onAbort = () => reject(new AbortError(undefined, { cause: signal.reason }));
signal.addEventListener('abort', onAbort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
});
try {
return await SafePromiseRace([opPromise, abortPromise]);
} finally {
signal.removeEventListener('abort', onAbort);
}
}

async function writeFileHandle(filehandle, data, signal, encoding) {
checkAborted(signal);
if (isCustomIterable(data)) {
Expand Down Expand Up @@ -1654,33 +1675,51 @@ async function symlink(target, path, type) {
}

async function fstat(handle, options = { bigint: false }) {
const result = await PromisePrototypeThen(
binding.fstat(handle.fd, options.bigint, kUsePromises),
undefined,
handleErrorFromBinding,
const signal = options?.signal;
if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
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 = { bigint: false }) {
const signal = options?.signal;
if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
checkAborted(signal);
path = getValidatedPath(path);
if (permission.isEnabled() && !permission.has('fs.read', path)) {
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 = { bigint: false, throwIfNoEntry: true }) {
const result = await PromisePrototypeThen(
binding.stat(getValidatedPath(path), options.bigint, kUsePromises, options.throwIfNoEntry),
undefined,
handleErrorFromBinding,
const signal = options?.signal;
if (signal !== undefined) validateAbortSignal(signal, 'options.signal');
checkAborted(signal);
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