UNPKG

7.44 kBJavaScriptView Raw
1'use strict'
2
3const fs = require('graceful-fs')
4const path = require('path')
5const assert = require('assert')
6
7const isWindows = (process.platform === 'win32')
8
9function defaults (options) {
10 const methods = [
11 'unlink',
12 'chmod',
13 'stat',
14 'lstat',
15 'rmdir',
16 'readdir'
17 ]
18 methods.forEach(m => {
19 options[m] = options[m] || fs[m]
20 m = m + 'Sync'
21 options[m] = options[m] || fs[m]
22 })
23
24 options.maxBusyTries = options.maxBusyTries || 3
25}
26
27function rimraf (p, options, cb) {
28 let busyTries = 0
29
30 if (typeof options === 'function') {
31 cb = options
32 options = {}
33 }
34
35 assert(p, 'rimraf: missing path')
36 assert.strictEqual(typeof p, 'string', 'rimraf: path should be a string')
37 assert.strictEqual(typeof cb, 'function', 'rimraf: callback function required')
38 assert(options, 'rimraf: invalid options argument provided')
39 assert.strictEqual(typeof options, 'object', 'rimraf: options should be object')
40
41 defaults(options)
42
43 rimraf_(p, options, function CB (er) {
44 if (er) {
45 if ((er.code === 'EBUSY' || er.code === 'ENOTEMPTY' || er.code === 'EPERM') &&
46 busyTries < options.maxBusyTries) {
47 busyTries++
48 const time = busyTries * 100
49 // try again, with the same exact callback as this one.
50 return setTimeout(() => rimraf_(p, options, CB), time)
51 }
52
53 // already gone
54 if (er.code === 'ENOENT') er = null
55 }
56
57 cb(er)
58 })
59}
60
61// Two possible strategies.
62// 1. Assume it's a file. unlink it, then do the dir stuff on EPERM or EISDIR
63// 2. Assume it's a directory. readdir, then do the file stuff on ENOTDIR
64//
65// Both result in an extra syscall when you guess wrong. However, there
66// are likely far more normal files in the world than directories. This
67// is based on the assumption that a the average number of files per
68// directory is >= 1.
69//
70// If anyone ever complains about this, then I guess the strategy could
71// be made configurable somehow. But until then, YAGNI.
72function rimraf_ (p, options, cb) {
73 assert(p)
74 assert(options)
75 assert(typeof cb === 'function')
76
77 // sunos lets the root user unlink directories, which is... weird.
78 // so we have to lstat here and make sure it's not a dir.
79 options.lstat(p, (er, st) => {
80 if (er && er.code === 'ENOENT') {
81 return cb(null)
82 }
83
84 // Windows can EPERM on stat. Life is suffering.
85 if (er && er.code === 'EPERM' && isWindows) {
86 return fixWinEPERM(p, options, er, cb)
87 }
88
89 if (st && st.isDirectory()) {
90 return rmdir(p, options, er, cb)
91 }
92
93 options.unlink(p, er => {
94 if (er) {
95 if (er.code === 'ENOENT') {
96 return cb(null)
97 }
98 if (er.code === 'EPERM') {
99 return (isWindows)
100 ? fixWinEPERM(p, options, er, cb)
101 : rmdir(p, options, er, cb)
102 }
103 if (er.code === 'EISDIR') {
104 return rmdir(p, options, er, cb)
105 }
106 }
107 return cb(er)
108 })
109 })
110}
111
112function fixWinEPERM (p, options, er, cb) {
113 assert(p)
114 assert(options)
115 assert(typeof cb === 'function')
116
117 options.chmod(p, 0o666, er2 => {
118 if (er2) {
119 cb(er2.code === 'ENOENT' ? null : er)
120 } else {
121 options.stat(p, (er3, stats) => {
122 if (er3) {
123 cb(er3.code === 'ENOENT' ? null : er)
124 } else if (stats.isDirectory()) {
125 rmdir(p, options, er, cb)
126 } else {
127 options.unlink(p, cb)
128 }
129 })
130 }
131 })
132}
133
134function fixWinEPERMSync (p, options, er) {
135 let stats
136
137 assert(p)
138 assert(options)
139
140 try {
141 options.chmodSync(p, 0o666)
142 } catch (er2) {
143 if (er2.code === 'ENOENT') {
144 return
145 } else {
146 throw er
147 }
148 }
149
150 try {
151 stats = options.statSync(p)
152 } catch (er3) {
153 if (er3.code === 'ENOENT') {
154 return
155 } else {
156 throw er
157 }
158 }
159
160 if (stats.isDirectory()) {
161 rmdirSync(p, options, er)
162 } else {
163 options.unlinkSync(p)
164 }
165}
166
167function rmdir (p, options, originalEr, cb) {
168 assert(p)
169 assert(options)
170 assert(typeof cb === 'function')
171
172 // try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS)
173 // if we guessed wrong, and it's not a directory, then
174 // raise the original error.
175 options.rmdir(p, er => {
176 if (er && (er.code === 'ENOTEMPTY' || er.code === 'EEXIST' || er.code === 'EPERM')) {
177 rmkids(p, options, cb)
178 } else if (er && er.code === 'ENOTDIR') {
179 cb(originalEr)
180 } else {
181 cb(er)
182 }
183 })
184}
185
186function rmkids (p, options, cb) {
187 assert(p)
188 assert(options)
189 assert(typeof cb === 'function')
190
191 options.readdir(p, (er, files) => {
192 if (er) return cb(er)
193
194 let n = files.length
195 let errState
196
197 if (n === 0) return options.rmdir(p, cb)
198
199 files.forEach(f => {
200 rimraf(path.join(p, f), options, er => {
201 if (errState) {
202 return
203 }
204 if (er) return cb(errState = er)
205 if (--n === 0) {
206 options.rmdir(p, cb)
207 }
208 })
209 })
210 })
211}
212
213// this looks simpler, and is strictly *faster*, but will
214// tie up the JavaScript thread and fail on excessively
215// deep directory trees.
216function rimrafSync (p, options) {
217 let st
218
219 options = options || {}
220 defaults(options)
221
222 assert(p, 'rimraf: missing path')
223 assert.strictEqual(typeof p, 'string', 'rimraf: path should be a string')
224 assert(options, 'rimraf: missing options')
225 assert.strictEqual(typeof options, 'object', 'rimraf: options should be object')
226
227 try {
228 st = options.lstatSync(p)
229 } catch (er) {
230 if (er.code === 'ENOENT') {
231 return
232 }
233
234 // Windows can EPERM on stat. Life is suffering.
235 if (er.code === 'EPERM' && isWindows) {
236 fixWinEPERMSync(p, options, er)
237 }
238 }
239
240 try {
241 // sunos lets the root user unlink directories, which is... weird.
242 if (st && st.isDirectory()) {
243 rmdirSync(p, options, null)
244 } else {
245 options.unlinkSync(p)
246 }
247 } catch (er) {
248 if (er.code === 'ENOENT') {
249 return
250 } else if (er.code === 'EPERM') {
251 return isWindows ? fixWinEPERMSync(p, options, er) : rmdirSync(p, options, er)
252 } else if (er.code !== 'EISDIR') {
253 throw er
254 }
255 rmdirSync(p, options, er)
256 }
257}
258
259function rmdirSync (p, options, originalEr) {
260 assert(p)
261 assert(options)
262
263 try {
264 options.rmdirSync(p)
265 } catch (er) {
266 if (er.code === 'ENOTDIR') {
267 throw originalEr
268 } else if (er.code === 'ENOTEMPTY' || er.code === 'EEXIST' || er.code === 'EPERM') {
269 rmkidsSync(p, options)
270 } else if (er.code !== 'ENOENT') {
271 throw er
272 }
273 }
274}
275
276function rmkidsSync (p, options) {
277 assert(p)
278 assert(options)
279 options.readdirSync(p).forEach(f => rimrafSync(path.join(p, f), options))
280
281 if (isWindows) {
282 // We only end up here once we got ENOTEMPTY at least once, and
283 // at this point, we are guaranteed to have removed all the kids.
284 // So, we know that it won't be ENOENT or ENOTDIR or anything else.
285 // try really hard to delete stuff on windows, because it has a
286 // PROFOUNDLY annoying habit of not closing handles promptly when
287 // files are deleted, resulting in spurious ENOTEMPTY errors.
288 const startTime = Date.now()
289 do {
290 try {
291 const ret = options.rmdirSync(p, options)
292 return ret
293 } catch {}
294 } while (Date.now() - startTime < 500) // give up after 500ms
295 } else {
296 const ret = options.rmdirSync(p, options)
297 return ret
298 }
299}
300
301module.exports = rimraf
302rimraf.sync = rimrafSync