Spaces:
Paused
Paused
| // 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 | |