// 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