trouble-in-terror-town/node_modules/@npmcli/fs/lib/rm/polyfill.js
Mikolaj 2bbacbea09 did some more work on networking and removed EOS in favor of LRM
did some more work on networking and removed EOS in favor of Light Reflective Mirror
2022-05-31 15:04:31 +02:00

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