From 5760716a45786d1d75f48d1fffca78d04a9f0f71 Mon Sep 17 00:00:00 2001 From: isaacs Date: Thu, 2 Mar 2023 15:39:07 -0800 Subject: [PATCH] make rimraf cancelable with AbortSignals Fix: #257 --- README.md | 5 ++++ src/index.ts | 5 ++-- src/rimraf-move-remove.ts | 6 +++++ src/rimraf-posix.ts | 6 +++++ src/rimraf-windows.ts | 11 +++++++++ src/use-native.ts | 9 +++---- tap-snapshots/test/index.js.test.cjs | 16 +++++++++---- test/rimraf-move-remove.js | 36 ++++++++++++++++++++++++++++ test/rimraf-posix.js | 24 +++++++++++++++++++ test/rimraf-windows.js | 35 +++++++++++++++++++++++++++ test/use-native.js | 9 +++++++ 11 files changed, 152 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c51081c3..a2b82568 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,11 @@ Options: delayed 33ms. - `retryDelay`: Native only. Time to wait between retries, using linear backoff. Default `100`. +- `signal` Pass in an AbortSignal to cancel the directory + removal. This is useful when removing large folder structures, + if you'd like to limit the amount of time spent. Using a + `signal` option prevents the use of Node's built-in `fs.rm` + because that implementation does not support abort signals. Any other options are provided to the native Node.js `fs.rm` implementation when that is used. diff --git a/src/index.ts b/src/index.ts index 4321ec8c..7074d23f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export interface RimrafOptions { retryDelay?: number backoff?: number maxBackoff?: number + signal?: AbortSignal } const typeOrUndef = (val: any, t: string) => @@ -74,13 +75,13 @@ export const moveRemove = Object.assign(wrap(rimrafMoveRemove), { }) export const rimrafSync = wrapSync((path, opt) => - useNativeSync() ? rimrafNativeSync(path, opt) : rimrafManualSync(path, opt) + useNativeSync(opt) ? rimrafNativeSync(path, opt) : rimrafManualSync(path, opt) ) export const sync = rimrafSync export const rimraf = Object.assign( wrap((path, opt) => - useNative() ? rimrafNative(path, opt) : rimrafManual(path, opt) + useNative(opt) ? rimrafNative(path, opt) : rimrafManual(path, opt) ), { // this weirdness because it's easier than explicitly declaring diff --git a/src/rimraf-move-remove.ts b/src/rimraf-move-remove.ts index 821eff4a..a9e7e15b 100644 --- a/src/rimraf-move-remove.ts +++ b/src/rimraf-move-remove.ts @@ -73,6 +73,9 @@ export const rimrafMoveRemove = async ( path: string, opt: RimrafOptions ): Promise => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } if (!opt.tmp) { return rimrafMoveRemove(path, { ...opt, tmp: await defaultTmp(path) }) } @@ -122,6 +125,9 @@ export const rimrafMoveRemoveSync = ( path: string, opt: RimrafOptions ): void => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } if (!opt.tmp) { return rimrafMoveRemoveSync(path, { ...opt, tmp: defaultTmpSync(path) }) } diff --git a/src/rimraf-posix.ts b/src/rimraf-posix.ts index 0f678938..f567bc9d 100644 --- a/src/rimraf-posix.ts +++ b/src/rimraf-posix.ts @@ -16,6 +16,9 @@ import { RimrafOptions } from '.' import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js' export const rimrafPosix = async (path: string, opt: RimrafOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } const entries = await readdirOrError(path) if (!Array.isArray(entries)) { if (entries.code === 'ENOENT') { @@ -41,6 +44,9 @@ export const rimrafPosix = async (path: string, opt: RimrafOptions) => { } export const rimrafPosixSync = (path: string, opt: RimrafOptions) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } const entries = readdirOrErrorSync(path) if (!Array.isArray(entries)) { if (entries.code === 'ENOENT') { diff --git a/src/rimraf-windows.ts b/src/rimraf-windows.ts index 176f3e7a..2e3245c7 100644 --- a/src/rimraf-windows.ts +++ b/src/rimraf-windows.ts @@ -27,6 +27,11 @@ const rimrafWindowsDirMoveRemoveFallback = async ( path: string, opt: RimrafOptions ) => { + /* c8 ignore start */ + if (opt?.signal?.aborted) { + throw opt.signal.reason + } + /* c8 ignore stop */ try { await rimrafWindowsDir(path, opt) } catch (er) { @@ -41,6 +46,9 @@ const rimrafWindowsDirMoveRemoveFallbackSync = ( path: string, opt: RimrafOptions ) => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } try { rimrafWindowsDirSync(path, opt) } catch (er) { @@ -61,6 +69,9 @@ export const rimrafWindows = async ( opt: RimrafOptions, state = START ): Promise => { + if (opt?.signal?.aborted) { + throw opt.signal.reason + } if (!states.has(state)) { throw new TypeError('invalid third argument passed to rimraf') } diff --git a/src/use-native.ts b/src/use-native.ts index 97642176..fba2583b 100644 --- a/src/use-native.ts +++ b/src/use-native.ts @@ -1,10 +1,11 @@ const version = process.env.__TESTING_RIMRAF_NODE_VERSION__ || process.version const versArr = version.replace(/^v/, '').split('.') const hasNative = +versArr[0] > 14 || (+versArr[0] === 14 && +versArr[1] >= 14) +import { RimrafOptions } from './index.js' // we do NOT use native by default on Windows, because Node's native // rm implementation is less advanced. Change this code if that changes. import platform from './platform.js' -export const useNative = - !hasNative || platform === 'win32' ? () => false : () => true -export const useNativeSync = - !hasNative || platform === 'win32' ? () => false : () => true +export const useNative: (opt?: RimrafOptions) => boolean = + !hasNative || platform === 'win32' ? () => false : opt => !opt?.signal +export const useNativeSync: (opt?: RimrafOptions) => boolean = + !hasNative || platform === 'win32' ? () => false : opt => !opt?.signal diff --git a/tap-snapshots/test/index.js.test.cjs b/tap-snapshots/test/index.js.test.cjs index fa1e9960..fa006ef9 100644 --- a/tap-snapshots/test/index.js.test.cjs +++ b/tap-snapshots/test/index.js.test.cjs @@ -19,7 +19,9 @@ Array [ ], Array [ "useNative", - undefined, + Object { + "a": 1, + }, ], Array [ "rimrafPosix", @@ -40,7 +42,9 @@ Array [ ], Array [ "useNativeSync", - undefined, + Object { + "a": 2, + }, ], Array [ "rimrafPosixSync", @@ -66,7 +70,9 @@ Array [ ], Array [ "useNative", - undefined, + Object { + "a": 1, + }, ], Array [ "rimrafNative", @@ -87,7 +93,9 @@ Array [ ], Array [ "useNativeSync", - undefined, + Object { + "a": 2, + }, ], Array [ "rimrafNativeSync", diff --git a/test/rimraf-move-remove.js b/test/rimraf-move-remove.js index d6d9c54f..31b9c5ce 100644 --- a/test/rimraf-move-remove.js +++ b/test/rimraf-move-remove.js @@ -500,3 +500,39 @@ t.test('rimraffing root, do not actually rmdir root', async t => { }) t.end() }) + +t.test( + 'abort if the signal says to', + { skip: typeof AbortController === 'undefined' }, + t => { + const { rimrafMoveRemove, rimrafMoveRemoveSync } = t.mock( + '../dist/cjs/src/rimraf-move-remove.js', + {} + ) + t.test('sync', t => { + const ac = new AbortController() + const { signal } = ac + ac.abort(new Error('aborted rimraf')) + const d = t.testdir(fixture) + t.throws(() => rimrafMoveRemoveSync(d, { signal })) + t.end() + }) + t.test('async', async t => { + const ac = new AbortController() + const { signal } = ac + const d = t.testdir(fixture) + const p = t.rejects(() => rimrafMoveRemove(d, { signal })) + ac.abort(new Error('aborted rimraf')) + await p + }) + t.test('async, pre-aborted', async t => { + const ac = new AbortController() + const { signal } = ac + const d = t.testdir(fixture) + ac.abort(new Error('aborted rimraf')) + await t.rejects(() => rimrafMoveRemove(d, { signal })) + }) + + t.end() + } +) diff --git a/test/rimraf-posix.js b/test/rimraf-posix.js index 6172a4ce..f80025db 100644 --- a/test/rimraf-posix.js +++ b/test/rimraf-posix.js @@ -205,3 +205,27 @@ t.test('rimraffing root, do not actually rmdir root', async t => { }) t.end() }) + +t.test('abort on signal', { skip: typeof AbortController === 'undefined' }, t => { + const { + rimrafPosix, + rimrafPosixSync, + } = require('../dist/cjs/src/rimraf-posix.js') + t.test('sync', t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + ac.abort(new Error('aborted rimraf')) + t.throws(() => rimrafPosixSync(d, { signal })) + t.end() + }) + t.test('async', async t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + const p = t.rejects(() => rimrafPosix(d, { signal })) + ac.abort(new Error('aborted rimraf')) + await p + }) + t.end() +}) diff --git a/test/rimraf-windows.js b/test/rimraf-windows.js index 719bff65..f492bdef 100644 --- a/test/rimraf-windows.js +++ b/test/rimraf-windows.js @@ -490,3 +490,38 @@ t.test('do not allow third arg', async t => { t.rejects(rimrafWindows(ROOT, {}, true)) t.throws(() => rimrafWindowsSync(ROOT, {}, true)) }) + +t.test( + 'abort on signal', + { skip: typeof AbortController === 'undefined' }, + t => { + const { + rimrafWindows, + rimrafWindowsSync, + } = require('../dist/cjs/src/rimraf-windows.js') + t.test('sync', t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + ac.abort(new Error('aborted rimraf')) + t.throws(() => rimrafWindowsSync(d, { signal })) + t.end() + }) + t.test('async', async t => { + const d = t.testdir(fixture) + const ac = new AbortController() + const { signal } = ac + const p = t.rejects(() => rimrafWindows(d, { signal })) + ac.abort(new Error('aborted rimraf')) + await p + }) + t.test('async, pre-aborted', async t => { + const ac = new AbortController() + const { signal } = ac + const d = t.testdir(fixture) + ac.abort(new Error('aborted rimraf')) + await t.rejects(() => rimrafWindows(d, { signal })) + }) + t.end() + } +) diff --git a/test/use-native.js b/test/use-native.js index 943c1bf7..aa3c362e 100644 --- a/test/use-native.js +++ b/test/use-native.js @@ -46,6 +46,15 @@ if (!process.env.__TESTING_RIMRAF_EXPECT_USE_NATIVE__) { }) } else { const expect = process.env.__TESTING_RIMRAF_EXPECT_USE_NATIVE__ === '1' + if (expect) { + // always need manual if a signal is passed in + const signal = + typeof AbortController !== 'undefined' ? new AbortController().signal : {} + //@ts-ignore + t.equal(useNative({ signal }), false) + //@ts-ignore + t.equal(useNativeSync({ signal }), false) + } t.equal(useNative(), expect) t.equal(useNativeSync(), expect) }