Skip to content

Commit

Permalink
make rimraf cancelable with AbortSignals
Browse files Browse the repository at this point in the history
Fix: #257
  • Loading branch information
isaacs committed Mar 3, 2023
1 parent 417cdc7 commit 5760716
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 10 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface RimrafOptions {
retryDelay?: number
backoff?: number
maxBackoff?: number
signal?: AbortSignal
}

const typeOrUndef = (val: any, t: string) =>
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/rimraf-move-remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const rimrafMoveRemove = async (
path: string,
opt: RimrafOptions
): Promise<void> => {
if (opt?.signal?.aborted) {
throw opt.signal.reason
}
if (!opt.tmp) {
return rimrafMoveRemove(path, { ...opt, tmp: await defaultTmp(path) })
}
Expand Down Expand Up @@ -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) })
}
Expand Down
6 changes: 6 additions & 0 deletions src/rimraf-posix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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') {
Expand Down
11 changes: 11 additions & 0 deletions src/rimraf-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -41,6 +46,9 @@ const rimrafWindowsDirMoveRemoveFallbackSync = (
path: string,
opt: RimrafOptions
) => {
if (opt?.signal?.aborted) {
throw opt.signal.reason
}
try {
rimrafWindowsDirSync(path, opt)
} catch (er) {
Expand All @@ -61,6 +69,9 @@ export const rimrafWindows = async (
opt: RimrafOptions,
state = START
): Promise<void> => {
if (opt?.signal?.aborted) {
throw opt.signal.reason
}
if (!states.has(state)) {
throw new TypeError('invalid third argument passed to rimraf')
}
Expand Down
9 changes: 5 additions & 4 deletions src/use-native.ts
Original file line number Diff line number Diff line change
@@ -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
16 changes: 12 additions & 4 deletions tap-snapshots/test/index.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ Array [
],
Array [
"useNative",
undefined,
Object {
"a": 1,
},
],
Array [
"rimrafPosix",
Expand All @@ -40,7 +42,9 @@ Array [
],
Array [
"useNativeSync",
undefined,
Object {
"a": 2,
},
],
Array [
"rimrafPosixSync",
Expand All @@ -66,7 +70,9 @@ Array [
],
Array [
"useNative",
undefined,
Object {
"a": 1,
},
],
Array [
"rimrafNative",
Expand All @@ -87,7 +93,9 @@ Array [
],
Array [
"useNativeSync",
undefined,
Object {
"a": 2,
},
],
Array [
"rimrafNativeSync",
Expand Down
36 changes: 36 additions & 0 deletions test/rimraf-move-remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
)
24 changes: 24 additions & 0 deletions test/rimraf-posix.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
35 changes: 35 additions & 0 deletions test/rimraf-windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
)
9 changes: 9 additions & 0 deletions test/use-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 5760716

Please sign in to comment.