diff options
author | Gar <gar+gh@danger.computer> | 2021-08-26 21:20:59 +0300 |
---|---|---|
committer | Gar <gar+gh@danger.computer> | 2021-08-26 21:20:59 +0300 |
commit | e63a942c685233fa546788981ed9c144220d50e1 (patch) | |
tree | 4115e9ae913ad1b688c35711418a528184c9b337 /node_modules/@npmcli/fs/lib/rm/polyfill.js | |
parent | 7af36bb9f8e5c9facaa8deb114b76368841fbc66 (diff) |
cacache@15.3.0
* feat: introduce @npmcli/fs for tmp dir methods
Diffstat (limited to 'node_modules/@npmcli/fs/lib/rm/polyfill.js')
-rw-r--r-- | node_modules/@npmcli/fs/lib/rm/polyfill.js | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/node_modules/@npmcli/fs/lib/rm/polyfill.js b/node_modules/@npmcli/fs/lib/rm/polyfill.js new file mode 100644 index 000000000..77196b76b --- /dev/null +++ b/node_modules/@npmcli/fs/lib/rm/polyfill.js @@ -0,0 +1,238 @@ +// this file is a modified version of the code in node core >=14.14.0 +// which is, in turn, a modified version of the rimraf module on npm +// node core changes: +// - Use of the assert module has been replaced with core's error system. +// - All code related to the glob dependency has been removed. +// - Bring your own custom fs module is not currently supported. +// - Some basic code cleanup. +// changes here: +// - remove all callback related code +// - drop sync support +// - change assertions back to non-internal methods (see options.js) +// - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows +const errnos = require('os').constants.errno +const { join } = require('path') +const fs = require('../fs.js') + +// error codes that mean we need to remove contents +const notEmptyCodes = new Set([ + 'ENOTEMPTY', + 'EEXIST', + 'EPERM', +]) + +// error codes we can retry later +const retryCodes = new Set([ + 'EBUSY', + 'EMFILE', + 'ENFILE', + 'ENOTEMPTY', + 'EPERM', +]) + +const isWindows = process.platform === 'win32' + +const defaultOptions = { + retryDelay: 100, + maxRetries: 0, + recursive: false, + force: false, +} + +// this is drastically simplified, but should be roughly equivalent to what +// node core throws +class ERR_FS_EISDIR extends Error { + constructor (path) { + super() + this.info = { + code: 'EISDIR', + message: 'is a directory', + path, + syscall: 'rm', + errno: errnos.EISDIR, + } + this.name = 'SystemError' + this.code = 'ERR_FS_EISDIR' + this.errno = errnos.EISDIR + this.syscall = 'rm' + this.path = path + this.message = `Path is a directory: ${this.syscall} returned ${this.info.code} (is a directory) ${path}` + } + + toString () { + return `${this.name} [${this.code}]: ${this.message}` + } +} + +class ENOTDIR extends Error { + constructor (path) { + super() + this.name = 'Error' + this.code = 'ENOTDIR' + this.errno = errnos.ENOTDIR + this.syscall = 'rmdir' + this.path = path + this.message = `not a directory, ${this.syscall} '${this.path}'` + } + + toString () { + return `${this.name}: ${this.code}: ${this.message}` + } +} + +// force is passed separately here because we respect it for the first entry +// into rimraf only, any further calls that are spawned as a result (i.e. to +// delete content within the target) will ignore ENOENT errors +const rimraf = async (path, options, isTop = false) => { + const force = isTop ? options.force : true + const stat = await fs.lstat(path) + .catch((err) => { + // we only ignore ENOENT if we're forcing this call + if (err.code === 'ENOENT' && force) { + return + } + + if (isWindows && err.code === 'EPERM') { + return fixEPERM(path, options, err, isTop) + } + + throw err + }) + + // no stat object here means either lstat threw an ENOENT, or lstat threw + // an EPERM and the fixPERM function took care of things. either way, we're + // already done, so return early + if (!stat) { + return + } + + if (stat.isDirectory()) { + return rmdir(path, options, null, isTop) + } + + return fs.unlink(path) + .catch((err) => { + if (err.code === 'ENOENT' && force) { + return + } + + if (err.code === 'EISDIR') { + return rmdir(path, options, err, isTop) + } + + if (err.code === 'EPERM') { + // in windows, we handle this through fixEPERM which will also try to + // delete things again. everywhere else since deleting the target as a + // file didn't work we go ahead and try to delete it as a directory + return isWindows + ? fixEPERM(path, options, err, isTop) + : rmdir(path, options, err, isTop) + } + + throw err + }) +} + +const fixEPERM = async (path, options, originalErr, isTop) => { + const force = isTop ? options.force : true + const targetMissing = await fs.chmod(path, 0o666) + .catch((err) => { + if (err.code === 'ENOENT' && force) { + return true + } + + throw originalErr + }) + + // got an ENOENT above, return now. no file = no problem + if (targetMissing) { + return + } + + // this function does its own lstat rather than calling rimraf again to avoid + // infinite recursion for a repeating EPERM + const stat = await fs.lstat(path) + .catch((err) => { + if (err.code === 'ENOENT' && force) { + return + } + + throw originalErr + }) + + if (!stat) { + return + } + + if (stat.isDirectory()) { + return rmdir(path, options, originalErr, isTop) + } + + return fs.unlink(path) +} + +const rmdir = async (path, options, originalErr, isTop) => { + if (!options.recursive && isTop) { + throw originalErr || new ERR_FS_EISDIR(path) + } + const force = isTop ? options.force : true + + return fs.rmdir(path) + .catch(async (err) => { + // in Windows, calling rmdir on a file path will fail with ENOENT rather + // than ENOTDIR. to determine if that's what happened, we have to do + // another lstat on the path. if the path isn't actually gone, we throw + // away the ENOENT and replace it with our own ENOTDIR + if (isWindows && err.code === 'ENOENT') { + const stillExists = await fs.lstat(path).then(() => true, () => false) + if (stillExists) { + err = new ENOTDIR(path) + } + } + + // not there, not a problem + if (err.code === 'ENOENT' && force) { + return + } + + // we may not have originalErr if lstat tells us our target is a + // directory but that changes before we actually remove it, so + // only throw it here if it's set + if (originalErr && err.code === 'ENOTDIR') { + throw originalErr + } + + // the directory isn't empty, remove the contents and try again + if (notEmptyCodes.has(err.code)) { + const files = await fs.readdir(path) + await Promise.all(files.map((file) => { + const target = join(path, file) + return rimraf(target, options) + })) + return fs.rmdir(path) + } + + throw err + }) +} + +const rm = async (path, opts) => { + const options = { ...defaultOptions, ...opts } + let retries = 0 + + const errHandler = async (err) => { + if (retryCodes.has(err.code) && ++retries < options.maxRetries) { + const delay = retries * options.retryDelay + await promiseTimeout(delay) + return rimraf(path, options, true).catch(errHandler) + } + + throw err + } + + return rimraf(path, options, true).catch(errHandler) +} + +const promiseTimeout = (ms) => new Promise((r) => setTimeout(r, ms)) + +module.exports = rm |