GeezerVison is showing github.com — fs: abort in-flight stat operations · nodejs/node@6673eba
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/fs.md +24Lines changed: 24 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1542,6 +1542,10 @@ link(2) documentation for more detail. | 1542 | 1542 | <!-- YAML | 1543 | 1543 | added: v10.0.0 | 1544 | 1544 | changes: | | 1545 | + - version: REPLACEME | | 1546 | + pr-url: https://github.com/nodejs/node/pull/63143 | | 1547 | + description: Accepts an additional `signal` option to allow aborting the | | 1548 | + operation. | 1545 | 1549 | - version: v10.5.0 | 1546 | 1550 | pr-url: https://github.com/nodejs/node/pull/20220 | 1547 | 1551 | description: Accepts an additional `options` object to specify whether | @@ -1552,6 +1556,8 @@ changes: | 1552 | 1556 | * `options` {Object} | 1553 | 1557 | * `bigint` {boolean} Whether the numeric values in the returned | 1554 | 1558 | {fs.Stats} object should be `bigint`. **Default:** `false`. | | 1559 | + * `signal` {AbortSignal} An AbortSignal to cancel the operation. | | 1560 | + **Default:** `undefined`. | 1555 | 1561 | * Returns: {Promise} Fulfills with the {fs.Stats} object for the given | 1556 | 1562 | symbolic link `path`. | 1557 | 1563 |
| @@ -2080,6 +2086,10 @@ Removes files and directories (modeled on the standard POSIX `rm` utility). | 2080 | 2086 | <!-- YAML | 2081 | 2087 | added: v10.0.0 | 2082 | 2088 | changes: | | 2089 | + - version: REPLACEME | | 2090 | + pr-url: https://github.com/nodejs/node/pull/63143 | | 2091 | + description: Accepts an additional `signal` option to allow aborting the | | 2092 | + operation. | 2083 | 2093 | - version: v25.7.0 | 2084 | 2094 | pr-url: https://github.com/nodejs/node/pull/61178 | 2085 | 2095 | description: Accepts a `throwIfNoEntry` option to specify whether | @@ -2097,6 +2107,8 @@ changes: | 2097 | 2107 | * `throwIfNoEntry` {boolean} Whether an exception will be thrown | 2098 | 2108 | if no file system entry exists, rather than returning `undefined`. | 2099 | 2109 | **Default:** `true`. | | 2110 | + * `signal` {AbortSignal} An AbortSignal to cancel the operation. | | 2111 | + **Default:** `undefined`. | 2100 | 2112 | * Returns: {Promise} Fulfills with the {fs.Stats} object for the | 2101 | 2113 | given `path`. | 2102 | 2114 |
| @@ -3437,6 +3449,10 @@ exception are given to the completion callback. | 3437 | 3449 | <!-- YAML | 3438 | 3450 | added: v0.1.95 | 3439 | 3451 | changes: | | 3452 | + - version: REPLACEME | | 3453 | + pr-url: https://github.com/nodejs/node/pull/63143 | | 3454 | + description: Accepts an additional `signal` option to allow aborting the | | 3455 | + operation. | 3440 | 3456 | - version: v18.0.0 | 3441 | 3457 | pr-url: https://github.com/nodejs/node/pull/41678 | 3442 | 3458 | description: Passing an invalid callback to the `callback` argument | @@ -3460,6 +3476,8 @@ changes: | 3460 | 3476 | * `options` {Object} | 3461 | 3477 | * `bigint` {boolean} Whether the numeric values in the returned | 3462 | 3478 | {fs.Stats} object should be `bigint`. **Default:** `false`. | | 3479 | + * `signal` {AbortSignal} An AbortSignal to cancel the operation. | | 3480 | + **Default:** `undefined`. | 3463 | 3481 | * `callback` {Function} | 3464 | 3482 | * `err` {Error} | 3465 | 3483 | * `stats` {fs.Stats} | @@ -3802,6 +3820,10 @@ exception are given to the completion callback. | 3802 | 3820 | <!-- YAML | 3803 | 3821 | added: v0.1.30 | 3804 | 3822 | changes: | | 3823 | + - version: REPLACEME | | 3824 | + pr-url: https://github.com/nodejs/node/pull/63143 | | 3825 | + description: Accepts an additional `signal` option to allow aborting the | | 3826 | + operation. | 3805 | 3827 | - version: v18.0.0 | 3806 | 3828 | pr-url: https://github.com/nodejs/node/pull/41678 | 3807 | 3829 | description: Passing an invalid callback to the `callback` argument | @@ -3829,6 +3851,8 @@ changes: | 3829 | 3851 | * `options` {Object} | 3830 | 3852 | * `bigint` {boolean} Whether the numeric values in the returned | 3831 | 3853 | {fs.Stats} object should be `bigint`. **Default:** `false`. | | 3854 | + * `signal` {AbortSignal} An AbortSignal to cancel the operation. | | 3855 | + **Default:** `undefined`. | 3832 | 3856 | * `callback` {Function} | 3833 | 3857 | * `err` {Error} | 3834 | 3858 | * `stats` {fs.Stats} | |
Collapse file Copy file name to clipboardExpand all lines: lib/fs.js +39-6Lines changed: 39 additions & 6 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -139,6 +139,7 @@ const { | 139 | 139 | const { | 140 | 140 | isInt32, | 141 | 141 | parseFileMode, | | 142 | + validateAbortSignal, | 142 | 143 | validateBoolean, | 143 | 144 | validateBuffer, | 144 | 145 | validateEncoding, | @@ -383,6 +384,25 @@ function checkAborted(signal, callback) { | 383 | 384 | return false; | 384 | 385 | } | 385 | 386 |
| | 387 | +function bindSignalToReq(req, signal, callback) { | | 388 | + if (!signal) { | | 389 | + req.oncomplete = callback; | | 390 | + return; | | 391 | + } | | 392 | + let aborted = false; | | 393 | + const onAbort = () => { | | 394 | + aborted = true; | | 395 | + callback(new AbortError(undefined, { cause: signal.reason })); | | 396 | + }; | | 397 | + kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; | | 398 | + signal.addEventListener('abort', onAbort, { __proto__: null, [kResistStopPropagation]: true }); | | 399 | + req.oncomplete = function(err, result) { | | 400 | + signal.removeEventListener('abort', onAbort); | | 401 | + if (aborted) return; | | 402 | + callback(err, result); | | 403 | + }; | | 404 | +} | | 405 | + | 386 | 406 | /** | 387 | 407 | * Asynchronously reads the entire contents of a file. | 388 | 408 | * @param {string | Buffer | URL | number} path | @@ -1954,7 +1974,7 @@ function readdirSync(path, options) { | 1954 | 1974 | * Invokes the callback with the `fs.Stats` | 1955 | 1975 | * for the file descriptor. | 1956 | 1976 | * @param {number} fd | 1957 | | - * @param {{ bigint?: boolean; }} [options] | | 1977 | + * @param {{ bigint?: boolean, signal?: AbortSignal }} [options] | 1958 | 1978 | * @param {( | 1959 | 1979 | * err?: Error, | 1960 | 1980 | * stats?: Stats | @@ -1965,23 +1985,28 @@ function fstat(fd, options = { __proto__: null, bigint: false }, callback) { | 1965 | 1985 | if (typeof options === 'function') { | 1966 | 1986 | callback = options; | 1967 | 1987 | options = kEmptyObject; | | 1988 | + } else if (options === null || typeof options !== 'object') { | | 1989 | + options = kEmptyObject; | 1968 | 1990 | } | 1969 | 1991 |
| 1970 | 1992 | const h = vfsState.handlers; | 1971 | 1993 | if (h !== null && vfsResult(h.fstat(fd, options), callback)) return; | 1972 | 1994 |
| 1973 | 1995 | callback = makeStatsCallback(callback); | 1974 | 1996 |
| | 1997 | + if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal'); | | 1998 | + if (checkAborted(options.signal, callback)) return; | | 1999 | + | 1975 | 2000 | const req = new FSReqCallback(options.bigint); | 1976 | | - req.oncomplete = callback; | | 2001 | + bindSignalToReq(req, options.signal, callback); | 1977 | 2002 | binding.fstat(fd, options.bigint, req); | 1978 | 2003 | } | 1979 | 2004 |
| 1980 | 2005 | /** | 1981 | 2006 | * Retrieves the `fs.Stats` for the symbolic link | 1982 | 2007 | * referred to by the `path`. | 1983 | 2008 | * @param {string | Buffer | URL} path | 1984 | | - * @param {{ bigint?: boolean; }} [options] | | 2009 | + * @param {{ bigint?: boolean, signal?: AbortSignal }} [options] | 1985 | 2010 | * @param {( | 1986 | 2011 | * err?: Error, | 1987 | 2012 | * stats?: Stats | @@ -1992,6 +2017,10 @@ function lstat(path, options = { __proto__: null, bigint: false }, callback) { | 1992 | 2017 | if (typeof options === 'function') { | 1993 | 2018 | callback = options; | 1994 | 2019 | options = kEmptyObject; | | 2020 | + } else if (options === null || typeof options !== 'object') { | | 2021 | + options = kEmptyObject; | | 2022 | + } else { | | 2023 | + options = getOptions(options, { bigint: false }); | 1995 | 2024 | } | 1996 | 2025 |
| 1997 | 2026 | const h = vfsState.handlers; | @@ -2005,8 +2034,11 @@ function lstat(path, options = { __proto__: null, bigint: false }, callback) { | 2005 | 2034 | return; | 2006 | 2035 | } | 2007 | 2036 |
| | 2037 | + if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal'); | | 2038 | + if (checkAborted(options.signal, callback)) return; | | 2039 | + | 2008 | 2040 | const req = new FSReqCallback(options.bigint); | 2009 | | - req.oncomplete = callback; | | 2041 | + bindSignalToReq(req, options.signal, callback); | 2010 | 2042 | binding.lstat(path, options.bigint, req); | 2011 | 2043 | } | 2012 | 2044 |
| @@ -2036,11 +2068,12 @@ function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: | 2036 | 2068 | callback = makeStatsCallback(callback); | 2037 | 2069 | path = getValidatedPath(path); | 2038 | 2070 |
| | 2071 | + if (options.signal !== undefined) validateAbortSignal(options.signal, 'options.signal'); | 2039 | 2072 | if (checkAborted(options.signal, callback)) return; | 2040 | 2073 |
| 2041 | 2074 | const req = new FSReqCallback(options.bigint); | 2042 | | - req.oncomplete = callback; | 2043 | | - binding.stat(getValidatedPath(path), options.bigint, req, options.throwIfNoEntry); | | 2075 | + bindSignalToReq(req, options.signal, callback); | | 2076 | + binding.stat(path, options.bigint, req, options.throwIfNoEntry); | 2044 | 2077 | } | 2045 | 2078 |
| 2046 | 2079 | function statfs(path, options = { __proto__: null, bigint: false }, callback) { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/fs/promises.js +50-12Lines changed: 50 additions & 12 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -12,8 +12,10 @@ const { | 12 | 12 | PromisePrototypeThen, | 13 | 13 | PromiseReject, | 14 | 14 | PromiseResolve, | | 15 | + PromiseWithResolvers, | 15 | 16 | SafeArrayIterator, | 16 | 17 | SafePromisePrototypeFinally, | | 18 | + SafePromiseRace, | 17 | 19 | Symbol, | 18 | 20 | SymbolAsyncDispose, | 19 | 21 | SymbolAsyncIterator, | @@ -1126,6 +1128,21 @@ function checkAborted(signal) { | 1126 | 1128 | throw new AbortError(undefined, { cause: signal.reason }); | 1127 | 1129 | } | 1128 | 1130 |
| | 1131 | +function rejectWithReason(signal, reject) { | | 1132 | + return () => { | | 1133 | + reject(new AbortError(undefined, { cause: signal.reason })); | | 1134 | + }; | | 1135 | +} | | 1136 | + | | 1137 | +async function raceWithSignal(opPromise, signal) { | | 1138 | + if (!signal) return opPromise; | | 1139 | + const { promise: abortPromise, reject } = PromiseWithResolvers(); | | 1140 | + // eslint-disable-next-line no-unused-vars | | 1141 | + using _ = EventEmitter.addAbortListener(signal, | | 1142 | + rejectWithReason(signal, reject)); | | 1143 | + return await SafePromiseRace([opPromise, abortPromise]); | | 1144 | +} | | 1145 | + | 1129 | 1146 | async function writeFileHandle(filehandle, data, signal, encoding) { | 1130 | 1147 | checkAborted(signal); | 1131 | 1148 | if (isCustomIterable(data)) { | @@ -1779,15 +1796,26 @@ async function symlink(target, path, type) { | 1779 | 1796 | } | 1780 | 1797 |
| 1781 | 1798 | async function fstat(handle, options = { __proto__: null, bigint: false }) { | 1782 | | - const result = await PromisePrototypeThen( | 1783 | | - binding.fstat(handle.fd, options.bigint, kUsePromises), | 1784 | | - undefined, | 1785 | | - handleErrorFromBinding, | | 1799 | + validateObject(options, 'options'); | | 1800 | + const { signal } = options; | | 1801 | + if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); | | 1802 | + checkAborted(signal); | | 1803 | + const result = await raceWithSignal( | | 1804 | + PromisePrototypeThen( | | 1805 | + binding.fstat(handle.fd, options.bigint, kUsePromises), | | 1806 | + undefined, | | 1807 | + handleErrorFromBinding, | | 1808 | + ), | | 1809 | + signal, | 1786 | 1810 | ); | 1787 | 1811 | return getStatsFromBinding(result); | 1788 | 1812 | } | 1789 | 1813 |
| 1790 | 1814 | async function lstat(path, options = { __proto__: null, bigint: false }) { | | 1815 | + validateObject(options, 'options'); | | 1816 | + const { signal } = options; | | 1817 | + if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); | | 1818 | + checkAborted(signal); | 1791 | 1819 | const h = vfsState.handlers; | 1792 | 1820 | if (h !== null) { | 1793 | 1821 | const promise = h.lstat(path, options); | @@ -1798,24 +1826,34 @@ async function lstat(path, options = { __proto__: null, bigint: false }) { | 1798 | 1826 | const resource = pathModule.toNamespacedPath(BufferIsBuffer(path) ? BufferToString(path) : path); | 1799 | 1827 | throw new ERR_ACCESS_DENIED('Access to this API has been restricted', 'FileSystemRead', resource); | 1800 | 1828 | } | 1801 | | - const result = await PromisePrototypeThen( | 1802 | | - binding.lstat(path, options.bigint, kUsePromises), | 1803 | | - undefined, | 1804 | | - handleErrorFromBinding, | | 1829 | + const result = await raceWithSignal( | | 1830 | + PromisePrototypeThen( | | 1831 | + binding.lstat(path, options.bigint, kUsePromises), | | 1832 | + undefined, | | 1833 | + handleErrorFromBinding, | | 1834 | + ), | | 1835 | + signal, | 1805 | 1836 | ); | 1806 | 1837 | return getStatsFromBinding(result); | 1807 | 1838 | } | 1808 | 1839 |
| 1809 | 1840 | async function stat(path, options = { __proto__: null, bigint: false, throwIfNoEntry: true }) { | | 1841 | + validateObject(options, 'options'); | | 1842 | + const { signal } = options; | | 1843 | + if (signal !== undefined) validateAbortSignal(signal, 'options.signal'); | | 1844 | + checkAborted(signal); | 1810 | 1845 | const h = vfsState.handlers; | 1811 | 1846 | if (h !== null) { | 1812 | 1847 | const promise = h.stat(path, options); | 1813 | 1848 | if (promise !== undefined) return await promise; | 1814 | 1849 | } | 1815 | | - const result = await PromisePrototypeThen( | 1816 | | - binding.stat(getValidatedPath(path), options.bigint, kUsePromises, options.throwIfNoEntry), | 1817 | | - undefined, | 1818 | | - handleErrorFromBinding, | | 1850 | + const result = await raceWithSignal( | | 1851 | + PromisePrototypeThen( | | 1852 | + binding.stat(getValidatedPath(path), options.bigint, kUsePromises, options.throwIfNoEntry), | | 1853 | + undefined, | | 1854 | + handleErrorFromBinding, | | 1855 | + ), | | 1856 | + signal, | 1819 | 1857 | ); | 1820 | 1858 |
| 1821 | 1859 | // Binding will resolve undefined if UV_ENOENT or UV_ENOTDIR and throwIfNoEntry is false | |
|