239 lines
6.4 KiB
JavaScript
239 lines
6.4 KiB
JavaScript
// 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
|