summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorcjihrig <cjihrig@gmail.com>2019-08-16 13:17:21 -0400
committerRich Trott <rtrott@gmail.com>2019-08-23 13:59:07 -0700
commit53816cce699d02fb28a49b258e1fbc474568bbf8 (patch)
treeb022ff51ac6aa0b0a55cc1a34241eb118d5c0f3a /lib
parent2b1bcba385af380e3eaffd44315c83d3c0201cfe (diff)
downloadandroid-node-v8-53816cce699d02fb28a49b258e1fbc474568bbf8.tar.gz
android-node-v8-53816cce699d02fb28a49b258e1fbc474568bbf8.tar.bz2
android-node-v8-53816cce699d02fb28a49b258e1fbc474568bbf8.zip
fs: add recursive option to rmdir()
This commit adds a recursive option to fs.rmdir(), fs.rmdirSync(), and fs.promises.rmdir(). The implementation is a port of the npm module rimraf. PR-URL: https://github.com/nodejs/node/pull/29168 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Roman Reiss <me@silverwind.io> Reviewed-By: Ben Coe <bencoe@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/fs.js36
-rw-r--r--lib/internal/fs/promises.js14
-rw-r--r--lib/internal/fs/rimraf.js252
-rw-r--r--lib/internal/fs/utils.js29
4 files changed, 324 insertions, 7 deletions
diff --git a/lib/fs.js b/lib/fs.js
index 37d1eb7917..5c7d907f5e 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -76,6 +76,7 @@ const {
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
+ validateRmdirOptions,
warnOnNonPortableTemplate
} = require('internal/fs/utils');
const {
@@ -100,6 +101,8 @@ let watchers;
let ReadFileContext;
let ReadStream;
let WriteStream;
+let rimraf;
+let rimrafSync;
// These have to be separate because of how graceful-fs happens to do it's
// monkeypatching.
@@ -736,16 +739,41 @@ function ftruncateSync(fd, len = 0) {
handleErrorFromBinding(ctx);
}
-function rmdir(path, callback) {
+
+function lazyLoadRimraf() {
+ if (rimraf === undefined)
+ ({ rimraf, rimrafSync } = require('internal/fs/rimraf'));
+}
+
+function rmdir(path, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+
callback = makeCallback(callback);
- path = getValidatedPath(path);
+ path = pathModule.toNamespacedPath(getValidatedPath(path));
+ options = validateRmdirOptions(options);
+
+ if (options.recursive) {
+ lazyLoadRimraf();
+ return rimraf(path, options, callback);
+ }
+
const req = new FSReqCallback();
req.oncomplete = callback;
- binding.rmdir(pathModule.toNamespacedPath(path), req);
+ binding.rmdir(path, req);
}
-function rmdirSync(path) {
+function rmdirSync(path, options) {
path = getValidatedPath(path);
+ options = validateRmdirOptions(options);
+
+ if (options.recursive) {
+ lazyLoadRimraf();
+ return rimrafSync(pathModule.toNamespacedPath(path), options);
+ }
+
const ctx = { path };
binding.rmdir(pathModule.toNamespacedPath(path), undefined, ctx);
handleErrorFromBinding(ctx);
diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js
index 7e6fae9273..5af2ffa763 100644
--- a/lib/internal/fs/promises.js
+++ b/lib/internal/fs/promises.js
@@ -18,6 +18,7 @@ const {
ERR_METHOD_NOT_IMPLEMENTED
} = require('internal/errors').codes;
const { isUint8Array } = require('internal/util/types');
+const { rimrafPromises } = require('internal/fs/rimraf');
const {
copyObject,
getDirents,
@@ -32,6 +33,7 @@ const {
validateBufferArray,
validateOffsetLengthRead,
validateOffsetLengthWrite,
+ validateRmdirOptions,
warnOnNonPortableTemplate
} = require('internal/fs/utils');
const {
@@ -300,9 +302,15 @@ async function ftruncate(handle, len = 0) {
return binding.ftruncate(handle.fd, len, kUsePromises);
}
-async function rmdir(path) {
- path = getValidatedPath(path);
- return binding.rmdir(pathModule.toNamespacedPath(path), kUsePromises);
+async function rmdir(path, options) {
+ path = pathModule.toNamespacedPath(getValidatedPath(path));
+ options = validateRmdirOptions(options);
+
+ if (options.recursive) {
+ return rimrafPromises(path, options);
+ }
+
+ return binding.rmdir(path, kUsePromises);
}
async function fdatasync(handle) {
diff --git a/lib/internal/fs/rimraf.js b/lib/internal/fs/rimraf.js
new file mode 100644
index 0000000000..73f783d1d2
--- /dev/null
+++ b/lib/internal/fs/rimraf.js
@@ -0,0 +1,252 @@
+// 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.
+'use strict';
+const {
+ chmod,
+ chmodSync,
+ lstat,
+ lstatSync,
+ readdir,
+ readdirSync,
+ rmdir,
+ rmdirSync,
+ stat,
+ statSync,
+ unlink,
+ unlinkSync
+} = require('fs');
+const { join } = require('path');
+const { setTimeout } = require('timers');
+const notEmptyErrorCodes = new Set(['ENOTEMPTY', 'EEXIST', 'EPERM']);
+const isWindows = process.platform === 'win32';
+const epermHandler = isWindows ? fixWinEPERM : _rmdir;
+const epermHandlerSync = isWindows ? fixWinEPERMSync : _rmdirSync;
+const numRetries = isWindows ? 100 : 1;
+
+
+function rimraf(path, options, callback) {
+ let timeout = 0; // For EMFILE handling.
+ let busyTries = 0;
+
+ _rimraf(path, options, function CB(err) {
+ if (err) {
+ if ((err.code === 'EBUSY' || err.code === 'ENOTEMPTY' ||
+ err.code === 'EPERM') && busyTries < options.maxBusyTries) {
+ busyTries++;
+ return setTimeout(_rimraf, busyTries * 100, path, options, CB);
+ }
+
+ if (err.code === 'EMFILE' && timeout < options.emfileWait)
+ return setTimeout(_rimraf, timeout++, 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) {
+ readdir(path, (err, files) => {
+ if (err)
+ return callback(err);
+
+ let numFiles = files.length;
+
+ if (numFiles === 0)
+ return rmdir(path, callback);
+
+ let done = false;
+
+ files.forEach((child) => {
+ rimraf(join(path, child), 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 !== undefined && stats.isDirectory())
+ _rmdirSync(path, options, null);
+ else
+ unlinkSync(path);
+ } 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 _rmdirSync(path, options, originalErr) {
+ try {
+ rmdirSync(path);
+ } catch (err) {
+ if (err.code === 'ENOENT')
+ return;
+ if (err.code === 'ENOTDIR')
+ throw originalErr;
+
+ 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.
+ readdirSync(path).forEach((child) => {
+ rimrafSync(join(path, child), options);
+ });
+
+ for (let i = 0; i < numRetries; i++) {
+ try {
+ return rmdirSync(path, options);
+ } catch {} // Ignore errors.
+ }
+ }
+ }
+}
+
+
+function fixWinEPERMSync(path, options, originalErr) {
+ try {
+ chmodSync(path, 0o666);
+ } catch (err) {
+ if (err.code === 'ENOENT')
+ return;
+
+ throw originalErr;
+ }
+
+ let stats;
+
+ try {
+ stats = statSync(path);
+ } catch (err) {
+ if (err.code === 'ENOENT')
+ return;
+
+ throw originalErr;
+ }
+
+ if (stats.isDirectory())
+ _rmdirSync(path, options, originalErr);
+ else
+ unlinkSync(path);
+}
+
+
+module.exports = { rimraf, rimrafPromises, rimrafSync };
diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js
index 600d118db3..fb060d23e6 100644
--- a/lib/internal/fs/utils.js
+++ b/lib/internal/fs/utils.js
@@ -22,6 +22,10 @@ const {
} = require('internal/util/types');
const { once } = require('internal/util');
const { toPathIfFileURL } = require('internal/url');
+const {
+ validateInt32,
+ validateUint32
+} = require('internal/validators');
const pathModule = require('path');
const kType = Symbol('type');
const kStats = Symbol('stats');
@@ -525,6 +529,30 @@ function warnOnNonPortableTemplate(template) {
}
}
+const defaultRmdirOptions = {
+ emfileWait: 1000,
+ maxBusyTries: 3,
+ recursive: false,
+};
+
+const validateRmdirOptions = hideStackFrames((options) => {
+ if (options === undefined)
+ return defaultRmdirOptions;
+ if (options === null || typeof options !== 'object')
+ throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
+
+ options = { ...defaultRmdirOptions, ...options };
+
+ if (typeof options.recursive !== 'boolean')
+ throw new ERR_INVALID_ARG_TYPE('recursive', 'boolean', options.recursive);
+
+ validateInt32(options.emfileWait, 'emfileWait', 0);
+ validateUint32(options.maxBusyTries, 'maxBusyTries');
+
+ return options;
+});
+
+
module.exports = {
assertEncoding,
BigIntStats, // for testing
@@ -545,5 +573,6 @@ module.exports = {
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
+ validateRmdirOptions,
warnOnNonPortableTemplate
};