diff options
author | cjihrig <cjihrig@gmail.com> | 2019-08-16 13:17:21 -0400 |
---|---|---|
committer | Rich Trott <rtrott@gmail.com> | 2019-08-23 13:59:07 -0700 |
commit | 53816cce699d02fb28a49b258e1fbc474568bbf8 (patch) | |
tree | b022ff51ac6aa0b0a55cc1a34241eb118d5c0f3a /lib | |
parent | 2b1bcba385af380e3eaffd44315c83d3c0201cfe (diff) | |
download | android-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.js | 36 | ||||
-rw-r--r-- | lib/internal/fs/promises.js | 14 | ||||
-rw-r--r-- | lib/internal/fs/rimraf.js | 252 | ||||
-rw-r--r-- | lib/internal/fs/utils.js | 29 |
4 files changed, 324 insertions, 7 deletions
@@ -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 }; |