You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					361 lines
				
				8.7 KiB
			
		
		
			
		
	
	
					361 lines
				
				8.7 KiB
			| 
											3 years ago
										 | const assert = require("assert") | ||
|  | const path = require("path") | ||
|  | const fs = require("fs") | ||
|  | let glob = undefined | ||
|  | try { | ||
|  |   glob = require("glob") | ||
|  | } catch (_err) { | ||
|  |   // treat glob as optional.
 | ||
|  | } | ||
|  | 
 | ||
|  | const defaultGlobOpts = { | ||
|  |   nosort: true, | ||
|  |   silent: true | ||
|  | } | ||
|  | 
 | ||
|  | // for EMFILE handling
 | ||
|  | let timeout = 0 | ||
|  | 
 | ||
|  | const isWindows = (process.platform === "win32") | ||
|  | 
 | ||
|  | const defaults = options => { | ||
|  |   const methods = [ | ||
|  |     'unlink', | ||
|  |     'chmod', | ||
|  |     'stat', | ||
|  |     'lstat', | ||
|  |     'rmdir', | ||
|  |     'readdir' | ||
|  |   ] | ||
|  |   methods.forEach(m => { | ||
|  |     options[m] = options[m] || fs[m] | ||
|  |     m = m + 'Sync' | ||
|  |     options[m] = options[m] || fs[m] | ||
|  |   }) | ||
|  | 
 | ||
|  |   options.maxBusyTries = options.maxBusyTries || 3 | ||
|  |   options.emfileWait = options.emfileWait || 1000 | ||
|  |   if (options.glob === false) { | ||
|  |     options.disableGlob = true | ||
|  |   } | ||
|  |   if (options.disableGlob !== true && glob === undefined) { | ||
|  |     throw Error('glob dependency not found, set `options.disableGlob = true` if intentional') | ||
|  |   } | ||
|  |   options.disableGlob = options.disableGlob || false | ||
|  |   options.glob = options.glob || defaultGlobOpts | ||
|  | } | ||
|  | 
 | ||
|  | const rimraf = (p, options, cb) => { | ||
|  |   if (typeof options === 'function') { | ||
|  |     cb = options | ||
|  |     options = {} | ||
|  |   } | ||
|  | 
 | ||
|  |   assert(p, 'rimraf: missing path') | ||
|  |   assert.equal(typeof p, 'string', 'rimraf: path should be a string') | ||
|  |   assert.equal(typeof cb, 'function', 'rimraf: callback function required') | ||
|  |   assert(options, 'rimraf: invalid options argument provided') | ||
|  |   assert.equal(typeof options, 'object', 'rimraf: options should be object') | ||
|  | 
 | ||
|  |   defaults(options) | ||
|  | 
 | ||
|  |   let busyTries = 0 | ||
|  |   let errState = null | ||
|  |   let n = 0 | ||
|  | 
 | ||
|  |   const next = (er) => { | ||
|  |     errState = errState || er | ||
|  |     if (--n === 0) | ||
|  |       cb(errState) | ||
|  |   } | ||
|  | 
 | ||
|  |   const afterGlob = (er, results) => { | ||
|  |     if (er) | ||
|  |       return cb(er) | ||
|  | 
 | ||
|  |     n = results.length | ||
|  |     if (n === 0) | ||
|  |       return cb() | ||
|  | 
 | ||
|  |     results.forEach(p => { | ||
|  |       const CB = (er) => { | ||
|  |         if (er) { | ||
|  |           if ((er.code === "EBUSY" || er.code === "ENOTEMPTY" || er.code === "EPERM") && | ||
|  |               busyTries < options.maxBusyTries) { | ||
|  |             busyTries ++ | ||
|  |             // try again, with the same exact callback as this one.
 | ||
|  |             return setTimeout(() => rimraf_(p, options, CB), busyTries * 100) | ||
|  |           } | ||
|  | 
 | ||
|  |           // this one won't happen if graceful-fs is used.
 | ||
|  |           if (er.code === "EMFILE" && timeout < options.emfileWait) { | ||
|  |             return setTimeout(() => rimraf_(p, options, CB), timeout ++) | ||
|  |           } | ||
|  | 
 | ||
|  |           // already gone
 | ||
|  |           if (er.code === "ENOENT") er = null | ||
|  |         } | ||
|  | 
 | ||
|  |         timeout = 0 | ||
|  |         next(er) | ||
|  |       } | ||
|  |       rimraf_(p, options, CB) | ||
|  |     }) | ||
|  |   } | ||
|  | 
 | ||
|  |   if (options.disableGlob || !glob.hasMagic(p)) | ||
|  |     return afterGlob(null, [p]) | ||
|  | 
 | ||
|  |   options.lstat(p, (er, stat) => { | ||
|  |     if (!er) | ||
|  |       return afterGlob(null, [p]) | ||
|  | 
 | ||
|  |     glob(p, options.glob, afterGlob) | ||
|  |   }) | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | // Two possible strategies.
 | ||
|  | // 1. Assume it's a file.  unlink it, then do the dir stuff on EPERM or EISDIR
 | ||
|  | // 2. Assume it's a directory.  readdir, then do the file stuff on ENOTDIR
 | ||
|  | //
 | ||
|  | // Both result in an extra syscall when you guess wrong.  However, there
 | ||
|  | // are likely far more normal files in the world than directories.  This
 | ||
|  | // is based on the assumption that a the average number of files per
 | ||
|  | // directory is >= 1.
 | ||
|  | //
 | ||
|  | // If anyone ever complains about this, then I guess the strategy could
 | ||
|  | // be made configurable somehow.  But until then, YAGNI.
 | ||
|  | const rimraf_ = (p, options, cb) => { | ||
|  |   assert(p) | ||
|  |   assert(options) | ||
|  |   assert(typeof cb === 'function') | ||
|  | 
 | ||
|  |   // sunos lets the root user unlink directories, which is... weird.
 | ||
|  |   // so we have to lstat here and make sure it's not a dir.
 | ||
|  |   options.lstat(p, (er, st) => { | ||
|  |     if (er && er.code === "ENOENT") | ||
|  |       return cb(null) | ||
|  | 
 | ||
|  |     // Windows can EPERM on stat.  Life is suffering.
 | ||
|  |     if (er && er.code === "EPERM" && isWindows) | ||
|  |       fixWinEPERM(p, options, er, cb) | ||
|  | 
 | ||
|  |     if (st && st.isDirectory()) | ||
|  |       return rmdir(p, options, er, cb) | ||
|  | 
 | ||
|  |     options.unlink(p, er => { | ||
|  |       if (er) { | ||
|  |         if (er.code === "ENOENT") | ||
|  |           return cb(null) | ||
|  |         if (er.code === "EPERM") | ||
|  |           return (isWindows) | ||
|  |             ? fixWinEPERM(p, options, er, cb) | ||
|  |             : rmdir(p, options, er, cb) | ||
|  |         if (er.code === "EISDIR") | ||
|  |           return rmdir(p, options, er, cb) | ||
|  |       } | ||
|  |       return cb(er) | ||
|  |     }) | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | const fixWinEPERM = (p, options, er, cb) => { | ||
|  |   assert(p) | ||
|  |   assert(options) | ||
|  |   assert(typeof cb === 'function') | ||
|  | 
 | ||
|  |   options.chmod(p, 0o666, er2 => { | ||
|  |     if (er2) | ||
|  |       cb(er2.code === "ENOENT" ? null : er) | ||
|  |     else | ||
|  |       options.stat(p, (er3, stats) => { | ||
|  |         if (er3) | ||
|  |           cb(er3.code === "ENOENT" ? null : er) | ||
|  |         else if (stats.isDirectory()) | ||
|  |           rmdir(p, options, er, cb) | ||
|  |         else | ||
|  |           options.unlink(p, cb) | ||
|  |       }) | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | const fixWinEPERMSync = (p, options, er) => { | ||
|  |   assert(p) | ||
|  |   assert(options) | ||
|  | 
 | ||
|  |   try { | ||
|  |     options.chmodSync(p, 0o666) | ||
|  |   } catch (er2) { | ||
|  |     if (er2.code === "ENOENT") | ||
|  |       return | ||
|  |     else | ||
|  |       throw er | ||
|  |   } | ||
|  | 
 | ||
|  |   let stats | ||
|  |   try { | ||
|  |     stats = options.statSync(p) | ||
|  |   } catch (er3) { | ||
|  |     if (er3.code === "ENOENT") | ||
|  |       return | ||
|  |     else | ||
|  |       throw er | ||
|  |   } | ||
|  | 
 | ||
|  |   if (stats.isDirectory()) | ||
|  |     rmdirSync(p, options, er) | ||
|  |   else | ||
|  |     options.unlinkSync(p) | ||
|  | } | ||
|  | 
 | ||
|  | const rmdir = (p, options, originalEr, cb) => { | ||
|  |   assert(p) | ||
|  |   assert(options) | ||
|  |   assert(typeof cb === 'function') | ||
|  | 
 | ||
|  |   // try to rmdir first, and only readdir on ENOTEMPTY or EEXIST (SunOS)
 | ||
|  |   // if we guessed wrong, and it's not a directory, then
 | ||
|  |   // raise the original error.
 | ||
|  |   options.rmdir(p, er => { | ||
|  |     if (er && (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM")) | ||
|  |       rmkids(p, options, cb) | ||
|  |     else if (er && er.code === "ENOTDIR") | ||
|  |       cb(originalEr) | ||
|  |     else | ||
|  |       cb(er) | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | const rmkids = (p, options, cb) => { | ||
|  |   assert(p) | ||
|  |   assert(options) | ||
|  |   assert(typeof cb === 'function') | ||
|  | 
 | ||
|  |   options.readdir(p, (er, files) => { | ||
|  |     if (er) | ||
|  |       return cb(er) | ||
|  |     let n = files.length | ||
|  |     if (n === 0) | ||
|  |       return options.rmdir(p, cb) | ||
|  |     let errState | ||
|  |     files.forEach(f => { | ||
|  |       rimraf(path.join(p, f), options, er => { | ||
|  |         if (errState) | ||
|  |           return | ||
|  |         if (er) | ||
|  |           return cb(errState = er) | ||
|  |         if (--n === 0) | ||
|  |           options.rmdir(p, cb) | ||
|  |       }) | ||
|  |     }) | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | // this looks simpler, and is strictly *faster*, but will
 | ||
|  | // tie up the JavaScript thread and fail on excessively
 | ||
|  | // deep directory trees.
 | ||
|  | const rimrafSync = (p, options) => { | ||
|  |   options = options || {} | ||
|  |   defaults(options) | ||
|  | 
 | ||
|  |   assert(p, 'rimraf: missing path') | ||
|  |   assert.equal(typeof p, 'string', 'rimraf: path should be a string') | ||
|  |   assert(options, 'rimraf: missing options') | ||
|  |   assert.equal(typeof options, 'object', 'rimraf: options should be object') | ||
|  | 
 | ||
|  |   let results | ||
|  | 
 | ||
|  |   if (options.disableGlob || !glob.hasMagic(p)) { | ||
|  |     results = [p] | ||
|  |   } else { | ||
|  |     try { | ||
|  |       options.lstatSync(p) | ||
|  |       results = [p] | ||
|  |     } catch (er) { | ||
|  |       results = glob.sync(p, options.glob) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   if (!results.length) | ||
|  |     return | ||
|  | 
 | ||
|  |   for (let i = 0; i < results.length; i++) { | ||
|  |     const p = results[i] | ||
|  | 
 | ||
|  |     let st | ||
|  |     try { | ||
|  |       st = options.lstatSync(p) | ||
|  |     } catch (er) { | ||
|  |       if (er.code === "ENOENT") | ||
|  |         return | ||
|  | 
 | ||
|  |       // Windows can EPERM on stat.  Life is suffering.
 | ||
|  |       if (er.code === "EPERM" && isWindows) | ||
|  |         fixWinEPERMSync(p, options, er) | ||
|  |     } | ||
|  | 
 | ||
|  |     try { | ||
|  |       // sunos lets the root user unlink directories, which is... weird.
 | ||
|  |       if (st && st.isDirectory()) | ||
|  |         rmdirSync(p, options, null) | ||
|  |       else | ||
|  |         options.unlinkSync(p) | ||
|  |     } catch (er) { | ||
|  |       if (er.code === "ENOENT") | ||
|  |         return | ||
|  |       if (er.code === "EPERM") | ||
|  |         return isWindows ? fixWinEPERMSync(p, options, er) : rmdirSync(p, options, er) | ||
|  |       if (er.code !== "EISDIR") | ||
|  |         throw er | ||
|  | 
 | ||
|  |       rmdirSync(p, options, er) | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const rmdirSync = (p, options, originalEr) => { | ||
|  |   assert(p) | ||
|  |   assert(options) | ||
|  | 
 | ||
|  |   try { | ||
|  |     options.rmdirSync(p) | ||
|  |   } catch (er) { | ||
|  |     if (er.code === "ENOENT") | ||
|  |       return | ||
|  |     if (er.code === "ENOTDIR") | ||
|  |       throw originalEr | ||
|  |     if (er.code === "ENOTEMPTY" || er.code === "EEXIST" || er.code === "EPERM") | ||
|  |       rmkidsSync(p, options) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | const rmkidsSync = (p, options) => { | ||
|  |   assert(p) | ||
|  |   assert(options) | ||
|  |   options.readdirSync(p).forEach(f => rimrafSync(path.join(p, f), options)) | ||
|  | 
 | ||
|  |   // We only end up here once we got ENOTEMPTY at least once, and
 | ||
|  |   // at this point, we are guaranteed to have removed all the kids.
 | ||
|  |   // So, we know that it won't be ENOENT or ENOTDIR or anything else.
 | ||
|  |   // try really hard to delete stuff on windows, because it has a
 | ||
|  |   // PROFOUNDLY annoying habit of not closing handles promptly when
 | ||
|  |   // files are deleted, resulting in spurious ENOTEMPTY errors.
 | ||
|  |   const retries = isWindows ? 100 : 1 | ||
|  |   let i = 0 | ||
|  |   do { | ||
|  |     let threw = true | ||
|  |     try { | ||
|  |       const ret = options.rmdirSync(p, options) | ||
|  |       threw = false | ||
|  |       return ret | ||
|  |     } finally { | ||
|  |       if (++i < retries && threw) | ||
|  |         continue | ||
|  |     } | ||
|  |   } while (true) | ||
|  | } | ||
|  | 
 | ||
|  | module.exports = rimraf | ||
|  | rimraf.sync = rimrafSync |