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