14
14
import { basename , parse , resolve } from 'path'
15
15
import { defaultTmp , defaultTmpSync } from './default-tmp.js'
16
16
import { ignoreENOENT , ignoreENOENTSync } from './ignore-enoent.js'
17
- import { lstatSync , promises , renameSync , rmdirSync , unlinkSync } from './fs.js'
17
+ import * as FS from './fs.js'
18
18
import { Dirent , Stats } from 'fs'
19
19
import { RimrafAsyncOptions , RimrafSyncOptions } from './index.js'
20
20
import { readdirOrError , readdirOrErrorSync } from './readdir-or-error.js'
21
21
import { fixEPERM , fixEPERMSync } from './fix-eperm.js'
22
22
import { errorCode } from './error.js'
23
- const { lstat, rename, unlink, rmdir } = promises
23
+ import { retryBusy , retryBusySync , codes } from './retry-busy.js'
24
+
25
+ type Tmp < T extends RimrafAsyncOptions | RimrafSyncOptions > = T & {
26
+ tmp : string
27
+ }
24
28
25
29
// crypto.randomBytes is much slower, and Math.random() is enough here
26
- const uniqueFilename = ( path : string ) => `.${ basename ( path ) } .${ Math . random ( ) } `
30
+ const uniqueName = ( path : string , tmp : string ) =>
31
+ resolve ( tmp , `.${ basename ( path ) } .${ Math . random ( ) } ` )
32
+
33
+ // moveRemove is the fallback on Windows and due to flaky EPERM errors
34
+ // if we are actually on Windows, then we add EPERM to the list of
35
+ // error codes that we treat as busy, as well as retrying all fs
36
+ // operations for EPERM only.
37
+ const isWin = process . platform === 'win32'
27
38
28
- const unlinkFixEPERM = fixEPERM ( unlink )
29
- const unlinkFixEPERMSync = fixEPERMSync ( unlinkSync )
39
+ // all fs functions are only retried for EPERM and only on windows
40
+ const retryFsCodes = isWin ? new Set ( [ 'EPERM' ] ) : undefined
41
+ const maybeRetry = < T , U extends RimrafAsyncOptions > (
42
+ fn : ( path : string , opt : U ) => Promise < T > ,
43
+ ) => ( retryFsCodes ? retryBusy ( fn , retryFsCodes ) : fn )
44
+ const maybeRetrySync = < T , U extends RimrafSyncOptions > (
45
+ fn : ( path : string , opt : U ) => T ,
46
+ ) => ( retryFsCodes ? retryBusySync ( fn , retryFsCodes ) : fn )
47
+ const rename = maybeRetry (
48
+ async ( path : string , opt : Tmp < RimrafAsyncOptions > ) => {
49
+ const newPath = uniqueName ( path , opt . tmp )
50
+ await FS . promises . rename ( path , newPath )
51
+ return newPath
52
+ } ,
53
+ )
54
+ const renameSync = maybeRetrySync (
55
+ ( path : string , opt : Tmp < RimrafSyncOptions > ) => {
56
+ const newPath = uniqueName ( path , opt . tmp )
57
+ FS . renameSync ( path , newPath )
58
+ return newPath
59
+ } ,
60
+ )
61
+ const readdir = maybeRetry ( readdirOrError )
62
+ const readdirSync = maybeRetrySync ( readdirOrErrorSync )
63
+ const lstat = maybeRetry ( FS . promises . lstat )
64
+ const lstatSync = maybeRetrySync ( FS . lstatSync )
65
+
66
+ // unlink and rmdir and always retryable regardless of platform
67
+ // but we add the EPERM error code as a busy signal on Windows only
68
+ const retryCodes = new Set ( [ ...codes , ...( retryFsCodes || [ ] ) ] )
69
+ const unlink = retryBusy ( fixEPERM ( FS . promises . unlink ) , retryCodes )
70
+ const unlinkSync = retryBusySync ( fixEPERMSync ( FS . unlinkSync ) , retryCodes )
71
+ const rmdir = retryBusy ( fixEPERM ( FS . promises . rmdir ) , retryCodes )
72
+ const rmdirSync = retryBusySync ( fixEPERMSync ( FS . rmdirSync ) , retryCodes )
30
73
31
74
export const rimrafMoveRemove = async (
32
75
path : string ,
33
- opt : RimrafAsyncOptions ,
76
+ { tmp , ... opt } : RimrafAsyncOptions ,
34
77
) => {
35
78
opt ?. signal ?. throwIfAborted ( )
79
+
80
+ tmp ??= await defaultTmp ( path )
81
+ if ( path === tmp && parse ( path ) . root !== path ) {
82
+ throw new Error ( 'cannot delete temp directory used for deletion' )
83
+ }
84
+
36
85
return (
37
86
( await ignoreENOENT (
38
- lstat ( path ) . then ( stat => rimrafMoveRemoveDir ( path , opt , stat ) ) ,
87
+ lstat ( path , opt ) . then ( stat =>
88
+ rimrafMoveRemoveDir ( path , { ...opt , tmp } , stat ) ,
89
+ ) ,
39
90
) ) ?? true
40
91
)
41
92
}
42
93
94
+ export const rimrafMoveRemoveSync = (
95
+ path : string ,
96
+ { tmp, ...opt } : RimrafSyncOptions ,
97
+ ) => {
98
+ opt ?. signal ?. throwIfAborted ( )
99
+
100
+ tmp ??= defaultTmpSync ( path )
101
+ if ( path === tmp && parse ( path ) . root !== path ) {
102
+ throw new Error ( 'cannot delete temp directory used for deletion' )
103
+ }
104
+
105
+ return (
106
+ ignoreENOENTSync ( ( ) =>
107
+ rimrafMoveRemoveDirSync ( path , { ...opt , tmp } , lstatSync ( path , opt ) ) ,
108
+ ) ?? true
109
+ )
110
+ }
111
+
43
112
const rimrafMoveRemoveDir = async (
44
113
path : string ,
45
- opt : RimrafAsyncOptions ,
114
+ opt : Tmp < RimrafAsyncOptions > ,
46
115
ent : Dirent | Stats ,
47
116
) : Promise < boolean > => {
48
117
opt ?. signal ?. throwIfAborted ( )
49
- if ( ! opt . tmp ) {
50
- return rimrafMoveRemoveDir (
51
- path ,
52
- { ...opt , tmp : await defaultTmp ( path ) } ,
53
- ent ,
54
- )
55
- }
56
- if ( path === opt . tmp && parse ( path ) . root !== path ) {
57
- throw new Error ( 'cannot delete temp directory used for deletion' )
58
- }
59
118
60
- const entries = ent . isDirectory ( ) ? await readdirOrError ( path ) : null
119
+ const entries = ent . isDirectory ( ) ? await readdir ( path , opt ) : null
61
120
if ( ! Array . isArray ( entries ) ) {
62
121
// this can only happen if lstat/readdir lied, or if the dir was
63
122
// swapped out with a file at just the right moment.
@@ -74,7 +133,7 @@ const rimrafMoveRemoveDir = async (
74
133
if ( opt . filter && ! ( await opt . filter ( path , ent ) ) ) {
75
134
return false
76
135
}
77
- await ignoreENOENT ( tmpUnlink ( path , opt . tmp , unlinkFixEPERM ) )
136
+ await ignoreENOENT ( rename ( path , opt ) . then ( p => unlink ( p , opt ) ) )
78
137
return true
79
138
}
80
139
@@ -98,49 +157,18 @@ const rimrafMoveRemoveDir = async (
98
157
if ( opt . filter && ! ( await opt . filter ( path , ent ) ) ) {
99
158
return false
100
159
}
101
- await ignoreENOENT ( tmpUnlink ( path , opt . tmp , rmdir ) )
160
+ await ignoreENOENT ( rename ( path , opt ) . then ( p => rmdir ( p , opt ) ) )
102
161
return true
103
162
}
104
163
105
- const tmpUnlink = async < T > (
106
- path : string ,
107
- tmp : string ,
108
- rm : ( p : string ) => Promise < T > ,
109
- ) => {
110
- const tmpFile = resolve ( tmp , uniqueFilename ( path ) )
111
- await rename ( path , tmpFile )
112
- return await rm ( tmpFile )
113
- }
114
-
115
- export const rimrafMoveRemoveSync = ( path : string , opt : RimrafSyncOptions ) => {
116
- opt ?. signal ?. throwIfAborted ( )
117
- return (
118
- ignoreENOENTSync ( ( ) =>
119
- rimrafMoveRemoveDirSync ( path , opt , lstatSync ( path ) ) ,
120
- ) ?? true
121
- )
122
- }
123
-
124
164
const rimrafMoveRemoveDirSync = (
125
165
path : string ,
126
- opt : RimrafSyncOptions ,
166
+ opt : Tmp < RimrafSyncOptions > ,
127
167
ent : Dirent | Stats ,
128
168
) : boolean => {
129
169
opt ?. signal ?. throwIfAborted ( )
130
- if ( ! opt . tmp ) {
131
- return rimrafMoveRemoveDirSync (
132
- path ,
133
- { ...opt , tmp : defaultTmpSync ( path ) } ,
134
- ent ,
135
- )
136
- }
137
- const tmp : string = opt . tmp
138
170
139
- if ( path === opt . tmp && parse ( path ) . root !== path ) {
140
- throw new Error ( 'cannot delete temp directory used for deletion' )
141
- }
142
-
143
- const entries = ent . isDirectory ( ) ? readdirOrErrorSync ( path ) : null
171
+ const entries = ent . isDirectory ( ) ? readdirSync ( path , opt ) : null
144
172
if ( ! Array . isArray ( entries ) ) {
145
173
// this can only happen if lstat/readdir lied, or if the dir was
146
174
// swapped out with a file at just the right moment.
@@ -157,7 +185,7 @@ const rimrafMoveRemoveDirSync = (
157
185
if ( opt . filter && ! opt . filter ( path , ent ) ) {
158
186
return false
159
187
}
160
- ignoreENOENTSync ( ( ) => tmpUnlinkSync ( path , tmp , unlinkFixEPERMSync ) )
188
+ ignoreENOENTSync ( ( ) => unlinkSync ( renameSync ( path , opt ) , opt ) )
161
189
return true
162
190
}
163
191
@@ -175,16 +203,6 @@ const rimrafMoveRemoveDirSync = (
175
203
if ( opt . filter && ! opt . filter ( path , ent ) ) {
176
204
return false
177
205
}
178
- ignoreENOENTSync ( ( ) => tmpUnlinkSync ( path , tmp , rmdirSync ) )
206
+ ignoreENOENTSync ( ( ) => rmdirSync ( renameSync ( path , opt ) , opt ) )
179
207
return true
180
208
}
181
-
182
- const tmpUnlinkSync = (
183
- path : string ,
184
- tmp : string ,
185
- rmSync : ( p : string ) => void ,
186
- ) => {
187
- const tmpFile = resolve ( tmp , uniqueFilename ( path ) )
188
- renameSync ( path , tmpFile )
189
- return rmSync ( tmpFile )
190
- }
0 commit comments