Skip to content

Commit 5760716

Browse files
committed
make rimraf cancelable with AbortSignals
Fix: #257
1 parent 417cdc7 commit 5760716

File tree

11 files changed

+152
-10
lines changed

11 files changed

+152
-10
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ Options:
6161
delayed 33ms.
6262
- `retryDelay`: Native only. Time to wait between retries, using
6363
linear backoff. Default `100`.
64+
- `signal` Pass in an AbortSignal to cancel the directory
65+
removal. This is useful when removing large folder structures,
66+
if you'd like to limit the amount of time spent. Using a
67+
`signal` option prevents the use of Node's built-in `fs.rm`
68+
because that implementation does not support abort signals.
6469

6570
Any other options are provided to the native Node.js `fs.rm` implementation
6671
when that is used.

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface RimrafOptions {
88
retryDelay?: number
99
backoff?: number
1010
maxBackoff?: number
11+
signal?: AbortSignal
1112
}
1213

1314
const typeOrUndef = (val: any, t: string) =>
@@ -74,13 +75,13 @@ export const moveRemove = Object.assign(wrap(rimrafMoveRemove), {
7475
})
7576

7677
export const rimrafSync = wrapSync((path, opt) =>
77-
useNativeSync() ? rimrafNativeSync(path, opt) : rimrafManualSync(path, opt)
78+
useNativeSync(opt) ? rimrafNativeSync(path, opt) : rimrafManualSync(path, opt)
7879
)
7980
export const sync = rimrafSync
8081

8182
export const rimraf = Object.assign(
8283
wrap((path, opt) =>
83-
useNative() ? rimrafNative(path, opt) : rimrafManual(path, opt)
84+
useNative(opt) ? rimrafNative(path, opt) : rimrafManual(path, opt)
8485
),
8586
{
8687
// this weirdness because it's easier than explicitly declaring

src/rimraf-move-remove.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export const rimrafMoveRemove = async (
7373
path: string,
7474
opt: RimrafOptions
7575
): Promise<void> => {
76+
if (opt?.signal?.aborted) {
77+
throw opt.signal.reason
78+
}
7679
if (!opt.tmp) {
7780
return rimrafMoveRemove(path, { ...opt, tmp: await defaultTmp(path) })
7881
}
@@ -122,6 +125,9 @@ export const rimrafMoveRemoveSync = (
122125
path: string,
123126
opt: RimrafOptions
124127
): void => {
128+
if (opt?.signal?.aborted) {
129+
throw opt.signal.reason
130+
}
125131
if (!opt.tmp) {
126132
return rimrafMoveRemoveSync(path, { ...opt, tmp: defaultTmpSync(path) })
127133
}

src/rimraf-posix.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { RimrafOptions } from '.'
1616
import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js'
1717

1818
export const rimrafPosix = async (path: string, opt: RimrafOptions) => {
19+
if (opt?.signal?.aborted) {
20+
throw opt.signal.reason
21+
}
1922
const entries = await readdirOrError(path)
2023
if (!Array.isArray(entries)) {
2124
if (entries.code === 'ENOENT') {
@@ -41,6 +44,9 @@ export const rimrafPosix = async (path: string, opt: RimrafOptions) => {
4144
}
4245

4346
export const rimrafPosixSync = (path: string, opt: RimrafOptions) => {
47+
if (opt?.signal?.aborted) {
48+
throw opt.signal.reason
49+
}
4450
const entries = readdirOrErrorSync(path)
4551
if (!Array.isArray(entries)) {
4652
if (entries.code === 'ENOENT') {

src/rimraf-windows.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ const rimrafWindowsDirMoveRemoveFallback = async (
2727
path: string,
2828
opt: RimrafOptions
2929
) => {
30+
/* c8 ignore start */
31+
if (opt?.signal?.aborted) {
32+
throw opt.signal.reason
33+
}
34+
/* c8 ignore stop */
3035
try {
3136
await rimrafWindowsDir(path, opt)
3237
} catch (er) {
@@ -41,6 +46,9 @@ const rimrafWindowsDirMoveRemoveFallbackSync = (
4146
path: string,
4247
opt: RimrafOptions
4348
) => {
49+
if (opt?.signal?.aborted) {
50+
throw opt.signal.reason
51+
}
4452
try {
4553
rimrafWindowsDirSync(path, opt)
4654
} catch (er) {
@@ -61,6 +69,9 @@ export const rimrafWindows = async (
6169
opt: RimrafOptions,
6270
state = START
6371
): Promise<void> => {
72+
if (opt?.signal?.aborted) {
73+
throw opt.signal.reason
74+
}
6475
if (!states.has(state)) {
6576
throw new TypeError('invalid third argument passed to rimraf')
6677
}

src/use-native.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
const version = process.env.__TESTING_RIMRAF_NODE_VERSION__ || process.version
22
const versArr = version.replace(/^v/, '').split('.')
33
const hasNative = +versArr[0] > 14 || (+versArr[0] === 14 && +versArr[1] >= 14)
4+
import { RimrafOptions } from './index.js'
45
// we do NOT use native by default on Windows, because Node's native
56
// rm implementation is less advanced. Change this code if that changes.
67
import platform from './platform.js'
7-
export const useNative =
8-
!hasNative || platform === 'win32' ? () => false : () => true
9-
export const useNativeSync =
10-
!hasNative || platform === 'win32' ? () => false : () => true
8+
export const useNative: (opt?: RimrafOptions) => boolean =
9+
!hasNative || platform === 'win32' ? () => false : opt => !opt?.signal
10+
export const useNativeSync: (opt?: RimrafOptions) => boolean =
11+
!hasNative || platform === 'win32' ? () => false : opt => !opt?.signal

tap-snapshots/test/index.js.test.cjs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ Array [
1919
],
2020
Array [
2121
"useNative",
22-
undefined,
22+
Object {
23+
"a": 1,
24+
},
2325
],
2426
Array [
2527
"rimrafPosix",
@@ -40,7 +42,9 @@ Array [
4042
],
4143
Array [
4244
"useNativeSync",
43-
undefined,
45+
Object {
46+
"a": 2,
47+
},
4448
],
4549
Array [
4650
"rimrafPosixSync",
@@ -66,7 +70,9 @@ Array [
6670
],
6771
Array [
6872
"useNative",
69-
undefined,
73+
Object {
74+
"a": 1,
75+
},
7076
],
7177
Array [
7278
"rimrafNative",
@@ -87,7 +93,9 @@ Array [
8793
],
8894
Array [
8995
"useNativeSync",
90-
undefined,
96+
Object {
97+
"a": 2,
98+
},
9199
],
92100
Array [
93101
"rimrafNativeSync",

test/rimraf-move-remove.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,39 @@ t.test('rimraffing root, do not actually rmdir root', async t => {
500500
})
501501
t.end()
502502
})
503+
504+
t.test(
505+
'abort if the signal says to',
506+
{ skip: typeof AbortController === 'undefined' },
507+
t => {
508+
const { rimrafMoveRemove, rimrafMoveRemoveSync } = t.mock(
509+
'../dist/cjs/src/rimraf-move-remove.js',
510+
{}
511+
)
512+
t.test('sync', t => {
513+
const ac = new AbortController()
514+
const { signal } = ac
515+
ac.abort(new Error('aborted rimraf'))
516+
const d = t.testdir(fixture)
517+
t.throws(() => rimrafMoveRemoveSync(d, { signal }))
518+
t.end()
519+
})
520+
t.test('async', async t => {
521+
const ac = new AbortController()
522+
const { signal } = ac
523+
const d = t.testdir(fixture)
524+
const p = t.rejects(() => rimrafMoveRemove(d, { signal }))
525+
ac.abort(new Error('aborted rimraf'))
526+
await p
527+
})
528+
t.test('async, pre-aborted', async t => {
529+
const ac = new AbortController()
530+
const { signal } = ac
531+
const d = t.testdir(fixture)
532+
ac.abort(new Error('aborted rimraf'))
533+
await t.rejects(() => rimrafMoveRemove(d, { signal }))
534+
})
535+
536+
t.end()
537+
}
538+
)

test/rimraf-posix.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,27 @@ t.test('rimraffing root, do not actually rmdir root', async t => {
205205
})
206206
t.end()
207207
})
208+
209+
t.test('abort on signal', { skip: typeof AbortController === 'undefined' }, t => {
210+
const {
211+
rimrafPosix,
212+
rimrafPosixSync,
213+
} = require('../dist/cjs/src/rimraf-posix.js')
214+
t.test('sync', t => {
215+
const d = t.testdir(fixture)
216+
const ac = new AbortController()
217+
const { signal } = ac
218+
ac.abort(new Error('aborted rimraf'))
219+
t.throws(() => rimrafPosixSync(d, { signal }))
220+
t.end()
221+
})
222+
t.test('async', async t => {
223+
const d = t.testdir(fixture)
224+
const ac = new AbortController()
225+
const { signal } = ac
226+
const p = t.rejects(() => rimrafPosix(d, { signal }))
227+
ac.abort(new Error('aborted rimraf'))
228+
await p
229+
})
230+
t.end()
231+
})

test/rimraf-windows.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,3 +490,38 @@ t.test('do not allow third arg', async t => {
490490
t.rejects(rimrafWindows(ROOT, {}, true))
491491
t.throws(() => rimrafWindowsSync(ROOT, {}, true))
492492
})
493+
494+
t.test(
495+
'abort on signal',
496+
{ skip: typeof AbortController === 'undefined' },
497+
t => {
498+
const {
499+
rimrafWindows,
500+
rimrafWindowsSync,
501+
} = require('../dist/cjs/src/rimraf-windows.js')
502+
t.test('sync', t => {
503+
const d = t.testdir(fixture)
504+
const ac = new AbortController()
505+
const { signal } = ac
506+
ac.abort(new Error('aborted rimraf'))
507+
t.throws(() => rimrafWindowsSync(d, { signal }))
508+
t.end()
509+
})
510+
t.test('async', async t => {
511+
const d = t.testdir(fixture)
512+
const ac = new AbortController()
513+
const { signal } = ac
514+
const p = t.rejects(() => rimrafWindows(d, { signal }))
515+
ac.abort(new Error('aborted rimraf'))
516+
await p
517+
})
518+
t.test('async, pre-aborted', async t => {
519+
const ac = new AbortController()
520+
const { signal } = ac
521+
const d = t.testdir(fixture)
522+
ac.abort(new Error('aborted rimraf'))
523+
await t.rejects(() => rimrafWindows(d, { signal }))
524+
})
525+
t.end()
526+
}
527+
)

0 commit comments

Comments
 (0)