nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
283 lines (235 loc) • 7.36 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/fs/rimraf.js
import { Buffer } from "nstdlib/lib/buffer";
import * as fs from "nstdlib/lib/fs";
import { sep } from "nstdlib/lib/path";
import { setTimeout } from "nstdlib/lib/timers";
import { sleep, isWindows } from "nstdlib/lib/internal/util";
// This file is a modified version of the rimraf module on npm. It has been
// modified in the following ways:
// - 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.
const {
chmod,
chmodSync,
lstat,
lstatSync,
readdir,
readdirSync,
rmdir,
rmdirSync,
stat,
statSync,
unlink,
unlinkSync,
} = fs;
const notEmptyErrorCodes = new Set(["ENOTEMPTY", "EEXIST", "EPERM"]);
const retryErrorCodes = new Set([
"EBUSY",
"EMFILE",
"ENFILE",
"ENOTEMPTY",
"EPERM",
]);
const epermHandler = isWindows ? fixWinEPERM : _rmdir;
const epermHandlerSync = isWindows ? fixWinEPERMSync : _rmdirSync;
const readdirEncoding = "buffer";
const separator = Buffer.from(sep);
function rimraf(path, options, callback) {
let retries = 0;
_rimraf(path, options, function CB(err) {
if (err) {
if (retryErrorCodes.has(err.code) && retries < options.maxRetries) {
retries++;
const delay = retries * options.retryDelay;
return setTimeout(_rimraf, delay, path, options, CB);
}
// The file is already gone.
if (err.code === "ENOENT") err = null;
}
callback(err);
});
}
function _rimraf(path, options, callback) {
// SunOS lets the root user unlink directories. Use lstat here to make sure
// it's not a directory.
lstat(path, (err, stats) => {
if (err) {
if (err.code === "ENOENT") return callback(null);
// Windows can EPERM on stat.
if (isWindows && err.code === "EPERM")
return fixWinEPERM(path, options, err, callback);
} else if (stats.isDirectory()) {
return _rmdir(path, options, err, callback);
}
unlink(path, (err) => {
if (err) {
if (err.code === "ENOENT") return callback(null);
if (err.code === "EISDIR") return _rmdir(path, options, err, callback);
if (err.code === "EPERM") {
return epermHandler(path, options, err, callback);
}
}
return callback(err);
});
});
}
function fixWinEPERM(path, options, originalErr, callback) {
chmod(path, 0o666, (err) => {
if (err) return callback(err.code === "ENOENT" ? null : originalErr);
stat(path, (err, stats) => {
if (err) return callback(err.code === "ENOENT" ? null : originalErr);
if (stats.isDirectory()) _rmdir(path, options, originalErr, callback);
else unlink(path, callback);
});
});
}
function _rmdir(path, options, originalErr, callback) {
rmdir(path, (err) => {
if (err) {
if (notEmptyErrorCodes.has(err.code))
return _rmchildren(path, options, callback);
if (err.code === "ENOTDIR") return callback(originalErr);
}
callback(err);
});
}
function _rmchildren(path, options, callback) {
const pathBuf = Buffer.from(path);
readdir(pathBuf, readdirEncoding, (err, files) => {
if (err) return callback(err);
let numFiles = files.length;
if (numFiles === 0) return rmdir(path, callback);
let done = false;
Array.prototype.forEach.call(files, (child) => {
const childPath = Buffer.concat([pathBuf, separator, child]);
rimraf(childPath, options, (err) => {
if (done) return;
if (err) {
done = true;
return callback(err);
}
numFiles--;
if (numFiles === 0) rmdir(path, callback);
});
});
});
}
function rimrafPromises(path, options) {
return new Promise((resolve, reject) => {
rimraf(path, options, (err) => {
if (err) return reject(err);
resolve();
});
});
}
function rimrafSync(path, options) {
let stats;
try {
stats = lstatSync(path);
} catch (err) {
if (err.code === "ENOENT") return;
// Windows can EPERM on stat.
if (isWindows && err.code === "EPERM") fixWinEPERMSync(path, options, err);
}
try {
// SunOS lets the root user unlink directories.
if (stats?.isDirectory()) _rmdirSync(path, options, null);
else _unlinkSync(path, options);
} catch (err) {
if (err.code === "ENOENT") return;
if (err.code === "EPERM") return epermHandlerSync(path, options, err);
if (err.code !== "EISDIR") throw err;
_rmdirSync(path, options, err);
}
}
function _unlinkSync(path, options) {
const tries = options.maxRetries + 1;
for (let i = 1; i <= tries; i++) {
try {
return unlinkSync(path);
} catch (err) {
// Only sleep if this is not the last try, and the delay is greater
// than zero, and an error was encountered that warrants a retry.
if (
retryErrorCodes.has(err.code) &&
i < tries &&
options.retryDelay > 0
) {
sleep(i * options.retryDelay);
} else if (err.code === "ENOENT") {
// The file is already gone.
return;
} else if (i === tries) {
throw err;
}
}
}
}
function _rmdirSync(path, options, originalErr) {
try {
rmdirSync(path);
} catch (err) {
if (err.code === "ENOENT") return;
if (err.code === "ENOTDIR") {
throw originalErr || err;
}
if (notEmptyErrorCodes.has(err.code)) {
// Removing failed. Try removing all children and then retrying the
// original removal. Windows has a habit of not closing handles promptly
// when files are deleted, resulting in spurious ENOTEMPTY failures. Work
// around that issue by retrying on Windows.
const pathBuf = Buffer.from(path);
Array.prototype.forEach.call(
readdirSync(pathBuf, readdirEncoding),
(child) => {
const childPath = Buffer.concat([pathBuf, separator, child]);
rimrafSync(childPath, options);
},
);
const tries = options.maxRetries + 1;
for (let i = 1; i <= tries; i++) {
try {
return fs.rmdirSync(path);
} catch (err) {
// Only sleep if this is not the last try, and the delay is greater
// than zero, and an error was encountered that warrants a retry.
if (
retryErrorCodes.has(err.code) &&
i < tries &&
options.retryDelay > 0
) {
sleep(i * options.retryDelay);
} else if (err.code === "ENOENT") {
// The file is already gone.
return;
} else if (i === tries) {
throw err;
}
}
}
}
throw originalErr || err;
}
}
function fixWinEPERMSync(path, options, originalErr) {
try {
chmodSync(path, 0o666);
} catch (err) {
if (err.code === "ENOENT") return;
throw originalErr;
}
let stats;
try {
stats = statSync(path, { throwIfNoEntry: false });
} catch {
throw originalErr;
}
if (stats === undefined) return;
if (stats.isDirectory()) _rmdirSync(path, options, originalErr);
else _unlinkSync(path, options);
}
export { rimraf };
export { rimrafPromises };
export { rimrafSync };