| 1 | // this file is a modified version of the code in node core >=14.14.0
|
|---|
| 2 | // which is, in turn, a modified version of the rimraf module on npm
|
|---|
| 3 | // node core changes:
|
|---|
| 4 | // - Use of the assert module has been replaced with core's error system.
|
|---|
| 5 | // - All code related to the glob dependency has been removed.
|
|---|
| 6 | // - Bring your own custom fs module is not currently supported.
|
|---|
| 7 | // - Some basic code cleanup.
|
|---|
| 8 | // changes here:
|
|---|
| 9 | // - remove all callback related code
|
|---|
| 10 | // - drop sync support
|
|---|
| 11 | // - change assertions back to non-internal methods (see options.js)
|
|---|
| 12 | // - throws ENOTDIR when rmdir gets an ENOENT for a path that exists in Windows
|
|---|
| 13 | const errnos = require('os').constants.errno
|
|---|
| 14 | const { join } = require('path')
|
|---|
| 15 | const fs = require('../fs.js')
|
|---|
| 16 |
|
|---|
| 17 | // error codes that mean we need to remove contents
|
|---|
| 18 | const notEmptyCodes = new Set([
|
|---|
| 19 | 'ENOTEMPTY',
|
|---|
| 20 | 'EEXIST',
|
|---|
| 21 | 'EPERM',
|
|---|
| 22 | ])
|
|---|
| 23 |
|
|---|
| 24 | // error codes we can retry later
|
|---|
| 25 | const retryCodes = new Set([
|
|---|
| 26 | 'EBUSY',
|
|---|
| 27 | 'EMFILE',
|
|---|
| 28 | 'ENFILE',
|
|---|
| 29 | 'ENOTEMPTY',
|
|---|
| 30 | 'EPERM',
|
|---|
| 31 | ])
|
|---|
| 32 |
|
|---|
| 33 | const isWindows = process.platform === 'win32'
|
|---|
| 34 |
|
|---|
| 35 | const defaultOptions = {
|
|---|
| 36 | retryDelay: 100,
|
|---|
| 37 | maxRetries: 0,
|
|---|
| 38 | recursive: false,
|
|---|
| 39 | force: false,
|
|---|
| 40 | }
|
|---|
| 41 |
|
|---|
| 42 | // this is drastically simplified, but should be roughly equivalent to what
|
|---|
| 43 | // node core throws
|
|---|
| 44 | class ERR_FS_EISDIR extends Error {
|
|---|
| 45 | constructor (path) {
|
|---|
| 46 | super()
|
|---|
| 47 | this.info = {
|
|---|
| 48 | code: 'EISDIR',
|
|---|
| 49 | message: 'is a directory',
|
|---|
| 50 | path,
|
|---|
| 51 | syscall: 'rm',
|
|---|
| 52 | errno: errnos.EISDIR,
|
|---|
| 53 | }
|
|---|
| 54 | this.name = 'SystemError'
|
|---|
| 55 | this.code = 'ERR_FS_EISDIR'
|
|---|
| 56 | this.errno = errnos.EISDIR
|
|---|
| 57 | this.syscall = 'rm'
|
|---|
| 58 | this.path = path
|
|---|
| 59 | this.message = `Path is a directory: ${this.syscall} returned ` +
|
|---|
| 60 | `${this.info.code} (is a directory) ${path}`
|
|---|
| 61 | }
|
|---|
| 62 |
|
|---|
| 63 | toString () {
|
|---|
| 64 | return `${this.name} [${this.code}]: ${this.message}`
|
|---|
| 65 | }
|
|---|
| 66 | }
|
|---|
| 67 |
|
|---|
| 68 | class ENOTDIR extends Error {
|
|---|
| 69 | constructor (path) {
|
|---|
| 70 | super()
|
|---|
| 71 | this.name = 'Error'
|
|---|
| 72 | this.code = 'ENOTDIR'
|
|---|
| 73 | this.errno = errnos.ENOTDIR
|
|---|
| 74 | this.syscall = 'rmdir'
|
|---|
| 75 | this.path = path
|
|---|
| 76 | this.message = `not a directory, ${this.syscall} '${this.path}'`
|
|---|
| 77 | }
|
|---|
| 78 |
|
|---|
| 79 | toString () {
|
|---|
| 80 | return `${this.name}: ${this.code}: ${this.message}`
|
|---|
| 81 | }
|
|---|
| 82 | }
|
|---|
| 83 |
|
|---|
| 84 | // force is passed separately here because we respect it for the first entry
|
|---|
| 85 | // into rimraf only, any further calls that are spawned as a result (i.e. to
|
|---|
| 86 | // delete content within the target) will ignore ENOENT errors
|
|---|
| 87 | const rimraf = async (path, options, isTop = false) => {
|
|---|
| 88 | const force = isTop ? options.force : true
|
|---|
| 89 | const stat = await fs.lstat(path)
|
|---|
| 90 | .catch((err) => {
|
|---|
| 91 | // we only ignore ENOENT if we're forcing this call
|
|---|
| 92 | if (err.code === 'ENOENT' && force) {
|
|---|
| 93 | return
|
|---|
| 94 | }
|
|---|
| 95 |
|
|---|
| 96 | if (isWindows && err.code === 'EPERM') {
|
|---|
| 97 | return fixEPERM(path, options, err, isTop)
|
|---|
| 98 | }
|
|---|
| 99 |
|
|---|
| 100 | throw err
|
|---|
| 101 | })
|
|---|
| 102 |
|
|---|
| 103 | // no stat object here means either lstat threw an ENOENT, or lstat threw
|
|---|
| 104 | // an EPERM and the fixPERM function took care of things. either way, we're
|
|---|
| 105 | // already done, so return early
|
|---|
| 106 | if (!stat) {
|
|---|
| 107 | return
|
|---|
| 108 | }
|
|---|
| 109 |
|
|---|
| 110 | if (stat.isDirectory()) {
|
|---|
| 111 | return rmdir(path, options, null, isTop)
|
|---|
| 112 | }
|
|---|
| 113 |
|
|---|
| 114 | return fs.unlink(path)
|
|---|
| 115 | .catch((err) => {
|
|---|
| 116 | if (err.code === 'ENOENT' && force) {
|
|---|
| 117 | return
|
|---|
| 118 | }
|
|---|
| 119 |
|
|---|
| 120 | if (err.code === 'EISDIR') {
|
|---|
| 121 | return rmdir(path, options, err, isTop)
|
|---|
| 122 | }
|
|---|
| 123 |
|
|---|
| 124 | if (err.code === 'EPERM') {
|
|---|
| 125 | // in windows, we handle this through fixEPERM which will also try to
|
|---|
| 126 | // delete things again. everywhere else since deleting the target as a
|
|---|
| 127 | // file didn't work we go ahead and try to delete it as a directory
|
|---|
| 128 | return isWindows
|
|---|
| 129 | ? fixEPERM(path, options, err, isTop)
|
|---|
| 130 | : rmdir(path, options, err, isTop)
|
|---|
| 131 | }
|
|---|
| 132 |
|
|---|
| 133 | throw err
|
|---|
| 134 | })
|
|---|
| 135 | }
|
|---|
| 136 |
|
|---|
| 137 | const fixEPERM = async (path, options, originalErr, isTop) => {
|
|---|
| 138 | const force = isTop ? options.force : true
|
|---|
| 139 | const targetMissing = await fs.chmod(path, 0o666)
|
|---|
| 140 | .catch((err) => {
|
|---|
| 141 | if (err.code === 'ENOENT' && force) {
|
|---|
| 142 | return true
|
|---|
| 143 | }
|
|---|
| 144 |
|
|---|
| 145 | throw originalErr
|
|---|
| 146 | })
|
|---|
| 147 |
|
|---|
| 148 | // got an ENOENT above, return now. no file = no problem
|
|---|
| 149 | if (targetMissing) {
|
|---|
| 150 | return
|
|---|
| 151 | }
|
|---|
| 152 |
|
|---|
| 153 | // this function does its own lstat rather than calling rimraf again to avoid
|
|---|
| 154 | // infinite recursion for a repeating EPERM
|
|---|
| 155 | const stat = await fs.lstat(path)
|
|---|
| 156 | .catch((err) => {
|
|---|
| 157 | if (err.code === 'ENOENT' && force) {
|
|---|
| 158 | return
|
|---|
| 159 | }
|
|---|
| 160 |
|
|---|
| 161 | throw originalErr
|
|---|
| 162 | })
|
|---|
| 163 |
|
|---|
| 164 | if (!stat) {
|
|---|
| 165 | return
|
|---|
| 166 | }
|
|---|
| 167 |
|
|---|
| 168 | if (stat.isDirectory()) {
|
|---|
| 169 | return rmdir(path, options, originalErr, isTop)
|
|---|
| 170 | }
|
|---|
| 171 |
|
|---|
| 172 | return fs.unlink(path)
|
|---|
| 173 | }
|
|---|
| 174 |
|
|---|
| 175 | const rmdir = async (path, options, originalErr, isTop) => {
|
|---|
| 176 | if (!options.recursive && isTop) {
|
|---|
| 177 | throw originalErr || new ERR_FS_EISDIR(path)
|
|---|
| 178 | }
|
|---|
| 179 | const force = isTop ? options.force : true
|
|---|
| 180 |
|
|---|
| 181 | return fs.rmdir(path)
|
|---|
| 182 | .catch(async (err) => {
|
|---|
| 183 | // in Windows, calling rmdir on a file path will fail with ENOENT rather
|
|---|
| 184 | // than ENOTDIR. to determine if that's what happened, we have to do
|
|---|
| 185 | // another lstat on the path. if the path isn't actually gone, we throw
|
|---|
| 186 | // away the ENOENT and replace it with our own ENOTDIR
|
|---|
| 187 | if (isWindows && err.code === 'ENOENT') {
|
|---|
| 188 | const stillExists = await fs.lstat(path).then(() => true, () => false)
|
|---|
| 189 | if (stillExists) {
|
|---|
| 190 | err = new ENOTDIR(path)
|
|---|
| 191 | }
|
|---|
| 192 | }
|
|---|
| 193 |
|
|---|
| 194 | // not there, not a problem
|
|---|
| 195 | if (err.code === 'ENOENT' && force) {
|
|---|
| 196 | return
|
|---|
| 197 | }
|
|---|
| 198 |
|
|---|
| 199 | // we may not have originalErr if lstat tells us our target is a
|
|---|
| 200 | // directory but that changes before we actually remove it, so
|
|---|
| 201 | // only throw it here if it's set
|
|---|
| 202 | if (originalErr && err.code === 'ENOTDIR') {
|
|---|
| 203 | throw originalErr
|
|---|
| 204 | }
|
|---|
| 205 |
|
|---|
| 206 | // the directory isn't empty, remove the contents and try again
|
|---|
| 207 | if (notEmptyCodes.has(err.code)) {
|
|---|
| 208 | const files = await fs.readdir(path)
|
|---|
| 209 | await Promise.all(files.map((file) => {
|
|---|
| 210 | const target = join(path, file)
|
|---|
| 211 | return rimraf(target, options)
|
|---|
| 212 | }))
|
|---|
| 213 | return fs.rmdir(path)
|
|---|
| 214 | }
|
|---|
| 215 |
|
|---|
| 216 | throw err
|
|---|
| 217 | })
|
|---|
| 218 | }
|
|---|
| 219 |
|
|---|
| 220 | const rm = async (path, opts) => {
|
|---|
| 221 | const options = { ...defaultOptions, ...opts }
|
|---|
| 222 | let retries = 0
|
|---|
| 223 |
|
|---|
| 224 | const errHandler = async (err) => {
|
|---|
| 225 | if (retryCodes.has(err.code) && ++retries < options.maxRetries) {
|
|---|
| 226 | const delay = retries * options.retryDelay
|
|---|
| 227 | await promiseTimeout(delay)
|
|---|
| 228 | return rimraf(path, options, true).catch(errHandler)
|
|---|
| 229 | }
|
|---|
| 230 |
|
|---|
| 231 | throw err
|
|---|
| 232 | }
|
|---|
| 233 |
|
|---|
| 234 | return rimraf(path, options, true).catch(errHandler)
|
|---|
| 235 | }
|
|---|
| 236 |
|
|---|
| 237 | const promiseTimeout = (ms) => new Promise((r) => setTimeout(r, ms))
|
|---|
| 238 |
|
|---|
| 239 | module.exports = rm
|
|---|