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>
This commit is contained in:
parent
2b1bcba385
commit
53816cce69
19
LICENSE
19
LICENSE
@ -1507,3 +1507,22 @@ The externally maintained libraries used by Node.js are:
|
|||||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
- rimraf, located at lib/internal/fs/rimraf.js, is licensed as follows:
|
||||||
|
"""
|
||||||
|
The ISC License
|
||||||
|
|
||||||
|
Copyright (c) Isaac Z. Schlueter and Contributors
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||||
|
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
"""
|
||||||
|
@ -3017,10 +3017,14 @@ changes:
|
|||||||
|
|
||||||
Synchronous rename(2). Returns `undefined`.
|
Synchronous rename(2). Returns `undefined`.
|
||||||
|
|
||||||
## fs.rmdir(path, callback)
|
## fs.rmdir(path[, options], callback)
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v0.0.2
|
added: v0.0.2
|
||||||
changes:
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/29168
|
||||||
|
description: The `recursive`, `maxBusyTries`, and `emfileWait` options are
|
||||||
|
now supported.
|
||||||
- version: v10.0.0
|
- version: v10.0.0
|
||||||
pr-url: https://github.com/nodejs/node/pull/12562
|
pr-url: https://github.com/nodejs/node/pull/12562
|
||||||
description: The `callback` parameter is no longer optional. Not passing
|
description: The `callback` parameter is no longer optional. Not passing
|
||||||
@ -3035,7 +3039,21 @@ changes:
|
|||||||
it will emit a deprecation warning with id DEP0013.
|
it will emit a deprecation warning with id DEP0013.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
> Stability: 1 - Recursive removal is experimental.
|
||||||
|
|
||||||
* `path` {string|Buffer|URL}
|
* `path` {string|Buffer|URL}
|
||||||
|
* `options` {Object}
|
||||||
|
* `emfileWait` {integer} If an `EMFILE` error is encountered, Node.js will
|
||||||
|
retry the operation with a linear backoff of 1ms longer on each try until the
|
||||||
|
timeout duration passes this limit. This option is ignored if the `recursive`
|
||||||
|
option is not `true`. **Default:** `1000`.
|
||||||
|
* `maxBusyTries` {integer} If an `EBUSY`, `ENOTEMPTY`, or `EPERM` error is
|
||||||
|
encountered, Node.js will retry the operation with a linear backoff wait of
|
||||||
|
100ms longer on each try. This option represents the number of retries. This
|
||||||
|
option is ignored if the `recursive` option is not `true`. **Default:** `3`.
|
||||||
|
* `recursive` {boolean} If `true`, perform a recursive directory removal. In
|
||||||
|
recursive mode, errors are not reported if `path` does not exist, and
|
||||||
|
operations are retried on failure. **Default:** `false`.
|
||||||
* `callback` {Function}
|
* `callback` {Function}
|
||||||
* `err` {Error}
|
* `err` {Error}
|
||||||
|
|
||||||
@ -3045,17 +3063,27 @@ to the completion callback.
|
|||||||
Using `fs.rmdir()` on a file (not a directory) results in an `ENOENT` error on
|
Using `fs.rmdir()` on a file (not a directory) results in an `ENOENT` error on
|
||||||
Windows and an `ENOTDIR` error on POSIX.
|
Windows and an `ENOTDIR` error on POSIX.
|
||||||
|
|
||||||
## fs.rmdirSync(path)
|
## fs.rmdirSync(path[, options])
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v0.1.21
|
added: v0.1.21
|
||||||
changes:
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/29168
|
||||||
|
description: The `recursive`, `maxBusyTries`, and `emfileWait` options are
|
||||||
|
now supported.
|
||||||
- version: v7.6.0
|
- version: v7.6.0
|
||||||
pr-url: https://github.com/nodejs/node/pull/10739
|
pr-url: https://github.com/nodejs/node/pull/10739
|
||||||
description: The `path` parameters can be a WHATWG `URL` object using
|
description: The `path` parameters can be a WHATWG `URL` object using
|
||||||
`file:` protocol. Support is currently still *experimental*.
|
`file:` protocol. Support is currently still *experimental*.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
> Stability: 1 - Recursive removal is experimental.
|
||||||
|
|
||||||
* `path` {string|Buffer|URL}
|
* `path` {string|Buffer|URL}
|
||||||
|
* `options` {Object}
|
||||||
|
* `recursive` {boolean} If `true`, perform a recursive directory removal. In
|
||||||
|
recursive mode, errors are not reported if `path` does not exist, and
|
||||||
|
operations are retried on failure. **Default:** `false`.
|
||||||
|
|
||||||
Synchronous rmdir(2). Returns `undefined`.
|
Synchronous rmdir(2). Returns `undefined`.
|
||||||
|
|
||||||
@ -4694,12 +4722,31 @@ added: v10.0.0
|
|||||||
Renames `oldPath` to `newPath` and resolves the `Promise` with no arguments
|
Renames `oldPath` to `newPath` and resolves the `Promise` with no arguments
|
||||||
upon success.
|
upon success.
|
||||||
|
|
||||||
### fsPromises.rmdir(path)
|
### fsPromises.rmdir(path[, options])
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v10.0.0
|
added: v10.0.0
|
||||||
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/29168
|
||||||
|
description: The `recursive`, `maxBusyTries`, and `emfileWait` options are
|
||||||
|
now supported.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
> Stability: 1 - Recursive removal is experimental.
|
||||||
|
|
||||||
* `path` {string|Buffer|URL}
|
* `path` {string|Buffer|URL}
|
||||||
|
* `options` {Object}
|
||||||
|
* `emfileWait` {integer} If an `EMFILE` error is encountered, Node.js will
|
||||||
|
retry the operation with a linear backoff of 1ms longer on each try until the
|
||||||
|
timeout duration passes this limit. This option is ignored if the `recursive`
|
||||||
|
option is not `true`. **Default:** `1000`.
|
||||||
|
* `maxBusyTries` {integer} If an `EBUSY`, `ENOTEMPTY`, or `EPERM` error is
|
||||||
|
encountered, Node.js will retry the operation with a linear backoff wait of
|
||||||
|
100ms longer on each try. This option represents the number of retries. This
|
||||||
|
option is ignored if the `recursive` option is not `true`. **Default:** `3`.
|
||||||
|
* `recursive` {boolean} If `true`, perform a recursive directory removal. In
|
||||||
|
recursive mode, errors are not reported if `path` does not exist, and
|
||||||
|
operations are retried on failure. **Default:** `false`.
|
||||||
* Returns: {Promise}
|
* Returns: {Promise}
|
||||||
|
|
||||||
Removes the directory identified by `path` then resolves the `Promise` with
|
Removes the directory identified by `path` then resolves the `Promise` with
|
||||||
@ -5193,7 +5240,7 @@ the file contents.
|
|||||||
[`fs.readdir()`]: #fs_fs_readdir_path_options_callback
|
[`fs.readdir()`]: #fs_fs_readdir_path_options_callback
|
||||||
[`fs.readdirSync()`]: #fs_fs_readdirsync_path_options
|
[`fs.readdirSync()`]: #fs_fs_readdirsync_path_options
|
||||||
[`fs.realpath()`]: #fs_fs_realpath_path_options_callback
|
[`fs.realpath()`]: #fs_fs_realpath_path_options_callback
|
||||||
[`fs.rmdir()`]: #fs_fs_rmdir_path_callback
|
[`fs.rmdir()`]: #fs_fs_rmdir_path_options_callback
|
||||||
[`fs.stat()`]: #fs_fs_stat_path_options_callback
|
[`fs.stat()`]: #fs_fs_stat_path_options_callback
|
||||||
[`fs.symlink()`]: #fs_fs_symlink_target_path_type_callback
|
[`fs.symlink()`]: #fs_fs_symlink_target_path_type_callback
|
||||||
[`fs.utimes()`]: #fs_fs_utimes_path_atime_mtime_callback
|
[`fs.utimes()`]: #fs_fs_utimes_path_atime_mtime_callback
|
||||||
|
42
lib/fs.js
42
lib/fs.js
@ -76,6 +76,7 @@ const {
|
|||||||
validateOffsetLengthRead,
|
validateOffsetLengthRead,
|
||||||
validateOffsetLengthWrite,
|
validateOffsetLengthWrite,
|
||||||
validatePath,
|
validatePath,
|
||||||
|
validateRmdirOptions,
|
||||||
warnOnNonPortableTemplate
|
warnOnNonPortableTemplate
|
||||||
} = require('internal/fs/utils');
|
} = require('internal/fs/utils');
|
||||||
const {
|
const {
|
||||||
@ -100,6 +101,8 @@ let watchers;
|
|||||||
let ReadFileContext;
|
let ReadFileContext;
|
||||||
let ReadStream;
|
let ReadStream;
|
||||||
let WriteStream;
|
let WriteStream;
|
||||||
|
let rimraf;
|
||||||
|
let rimrafSync;
|
||||||
|
|
||||||
// These have to be separate because of how graceful-fs happens to do it's
|
// These have to be separate because of how graceful-fs happens to do it's
|
||||||
// monkeypatching.
|
// monkeypatching.
|
||||||
@ -736,16 +739,41 @@ function ftruncateSync(fd, len = 0) {
|
|||||||
handleErrorFromBinding(ctx);
|
handleErrorFromBinding(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rmdir(path, callback) {
|
|
||||||
callback = makeCallback(callback);
|
function lazyLoadRimraf() {
|
||||||
path = getValidatedPath(path);
|
if (rimraf === undefined)
|
||||||
const req = new FSReqCallback();
|
({ rimraf, rimrafSync } = require('internal/fs/rimraf'));
|
||||||
req.oncomplete = callback;
|
|
||||||
binding.rmdir(pathModule.toNamespacedPath(path), req);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rmdirSync(path) {
|
function rmdir(path, options, callback) {
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
callback = options;
|
||||||
|
options = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback = makeCallback(callback);
|
||||||
|
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(path, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rmdirSync(path, options) {
|
||||||
path = getValidatedPath(path);
|
path = getValidatedPath(path);
|
||||||
|
options = validateRmdirOptions(options);
|
||||||
|
|
||||||
|
if (options.recursive) {
|
||||||
|
lazyLoadRimraf();
|
||||||
|
return rimrafSync(pathModule.toNamespacedPath(path), options);
|
||||||
|
}
|
||||||
|
|
||||||
const ctx = { path };
|
const ctx = { path };
|
||||||
binding.rmdir(pathModule.toNamespacedPath(path), undefined, ctx);
|
binding.rmdir(pathModule.toNamespacedPath(path), undefined, ctx);
|
||||||
handleErrorFromBinding(ctx);
|
handleErrorFromBinding(ctx);
|
||||||
|
@ -18,6 +18,7 @@ const {
|
|||||||
ERR_METHOD_NOT_IMPLEMENTED
|
ERR_METHOD_NOT_IMPLEMENTED
|
||||||
} = require('internal/errors').codes;
|
} = require('internal/errors').codes;
|
||||||
const { isUint8Array } = require('internal/util/types');
|
const { isUint8Array } = require('internal/util/types');
|
||||||
|
const { rimrafPromises } = require('internal/fs/rimraf');
|
||||||
const {
|
const {
|
||||||
copyObject,
|
copyObject,
|
||||||
getDirents,
|
getDirents,
|
||||||
@ -32,6 +33,7 @@ const {
|
|||||||
validateBufferArray,
|
validateBufferArray,
|
||||||
validateOffsetLengthRead,
|
validateOffsetLengthRead,
|
||||||
validateOffsetLengthWrite,
|
validateOffsetLengthWrite,
|
||||||
|
validateRmdirOptions,
|
||||||
warnOnNonPortableTemplate
|
warnOnNonPortableTemplate
|
||||||
} = require('internal/fs/utils');
|
} = require('internal/fs/utils');
|
||||||
const {
|
const {
|
||||||
@ -300,9 +302,15 @@ async function ftruncate(handle, len = 0) {
|
|||||||
return binding.ftruncate(handle.fd, len, kUsePromises);
|
return binding.ftruncate(handle.fd, len, kUsePromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function rmdir(path) {
|
async function rmdir(path, options) {
|
||||||
path = getValidatedPath(path);
|
path = pathModule.toNamespacedPath(getValidatedPath(path));
|
||||||
return binding.rmdir(pathModule.toNamespacedPath(path), kUsePromises);
|
options = validateRmdirOptions(options);
|
||||||
|
|
||||||
|
if (options.recursive) {
|
||||||
|
return rimrafPromises(path, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.rmdir(path, kUsePromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fdatasync(handle) {
|
async function fdatasync(handle) {
|
||||||
|
252
lib/internal/fs/rimraf.js
Normal file
252
lib/internal/fs/rimraf.js
Normal file
@ -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 };
|
@ -22,6 +22,10 @@ const {
|
|||||||
} = require('internal/util/types');
|
} = require('internal/util/types');
|
||||||
const { once } = require('internal/util');
|
const { once } = require('internal/util');
|
||||||
const { toPathIfFileURL } = require('internal/url');
|
const { toPathIfFileURL } = require('internal/url');
|
||||||
|
const {
|
||||||
|
validateInt32,
|
||||||
|
validateUint32
|
||||||
|
} = require('internal/validators');
|
||||||
const pathModule = require('path');
|
const pathModule = require('path');
|
||||||
const kType = Symbol('type');
|
const kType = Symbol('type');
|
||||||
const kStats = Symbol('stats');
|
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 = {
|
module.exports = {
|
||||||
assertEncoding,
|
assertEncoding,
|
||||||
BigIntStats, // for testing
|
BigIntStats, // for testing
|
||||||
@ -545,5 +573,6 @@ module.exports = {
|
|||||||
validateOffsetLengthRead,
|
validateOffsetLengthRead,
|
||||||
validateOffsetLengthWrite,
|
validateOffsetLengthWrite,
|
||||||
validatePath,
|
validatePath,
|
||||||
|
validateRmdirOptions,
|
||||||
warnOnNonPortableTemplate
|
warnOnNonPortableTemplate
|
||||||
};
|
};
|
||||||
|
1
node.gyp
1
node.gyp
@ -124,6 +124,7 @@
|
|||||||
'lib/internal/freeze_intrinsics.js',
|
'lib/internal/freeze_intrinsics.js',
|
||||||
'lib/internal/fs/promises.js',
|
'lib/internal/fs/promises.js',
|
||||||
'lib/internal/fs/read_file_context.js',
|
'lib/internal/fs/read_file_context.js',
|
||||||
|
'lib/internal/fs/rimraf.js',
|
||||||
'lib/internal/fs/streams.js',
|
'lib/internal/fs/streams.js',
|
||||||
'lib/internal/fs/sync_write_stream.js',
|
'lib/internal/fs/sync_write_stream.js',
|
||||||
'lib/internal/fs/utils.js',
|
'lib/internal/fs/utils.js',
|
||||||
|
152
test/parallel/test-fs-rmdir-recursive.js
Normal file
152
test/parallel/test-fs-rmdir-recursive.js
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
// Flags: --expose-internals
|
||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
const tmpdir = require('../common/tmpdir');
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { validateRmdirOptions } = require('internal/fs/utils');
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
tmpdir.refresh();
|
||||||
|
|
||||||
|
function makeNonEmptyDirectory() {
|
||||||
|
const dirname = `rmdir-recursive-${count}`;
|
||||||
|
fs.mkdirSync(path.join(dirname, 'foo', 'bar', 'baz'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(dirname, 'text.txt'), 'hello', 'utf8');
|
||||||
|
count++;
|
||||||
|
return dirname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the asynchronous version.
|
||||||
|
{
|
||||||
|
const dir = makeNonEmptyDirectory();
|
||||||
|
|
||||||
|
// Removal should fail without the recursive option.
|
||||||
|
fs.rmdir(dir, common.mustCall((err) => {
|
||||||
|
assert.strictEqual(err.syscall, 'rmdir');
|
||||||
|
|
||||||
|
// Removal should fail without the recursive option set to true.
|
||||||
|
fs.rmdir(dir, { recursive: false }, common.mustCall((err) => {
|
||||||
|
assert.strictEqual(err.syscall, 'rmdir');
|
||||||
|
|
||||||
|
// Recursive removal should succeed.
|
||||||
|
fs.rmdir(dir, { recursive: true }, common.mustCall((err) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
// No error should occur if recursive and the directory does not exist.
|
||||||
|
fs.rmdir(dir, { recursive: true }, common.mustCall((err) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
// Attempted removal should fail now because the directory is gone.
|
||||||
|
fs.rmdir(dir, common.mustCall((err) => {
|
||||||
|
assert.strictEqual(err.syscall, 'rmdir');
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the synchronous version.
|
||||||
|
{
|
||||||
|
const dir = makeNonEmptyDirectory();
|
||||||
|
|
||||||
|
// Removal should fail without the recursive option set to true.
|
||||||
|
common.expectsError(() => {
|
||||||
|
fs.rmdirSync(dir);
|
||||||
|
}, { syscall: 'rmdir' });
|
||||||
|
common.expectsError(() => {
|
||||||
|
fs.rmdirSync(dir, { recursive: false });
|
||||||
|
}, { syscall: 'rmdir' });
|
||||||
|
|
||||||
|
// Recursive removal should succeed.
|
||||||
|
fs.rmdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
// No error should occur if recursive and the directory does not exist.
|
||||||
|
fs.rmdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
// Attempted removal should fail now because the directory is gone.
|
||||||
|
common.expectsError(() => fs.rmdirSync(dir), { syscall: 'rmdir' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the Promises based version.
|
||||||
|
(async () => {
|
||||||
|
const dir = makeNonEmptyDirectory();
|
||||||
|
|
||||||
|
// Removal should fail without the recursive option set to true.
|
||||||
|
assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
|
||||||
|
assert.rejects(fs.promises.rmdir(dir, { recursive: false }), {
|
||||||
|
syscall: 'rmdir'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recursive removal should succeed.
|
||||||
|
await fs.promises.rmdir(dir, { recursive: true });
|
||||||
|
|
||||||
|
// No error should occur if recursive and the directory does not exist.
|
||||||
|
await fs.promises.rmdir(dir, { recursive: true });
|
||||||
|
|
||||||
|
// Attempted removal should fail now because the directory is gone.
|
||||||
|
assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Test input validation.
|
||||||
|
{
|
||||||
|
const defaults = {
|
||||||
|
emfileWait: 1000,
|
||||||
|
maxBusyTries: 3,
|
||||||
|
recursive: false
|
||||||
|
};
|
||||||
|
const modified = {
|
||||||
|
emfileWait: 953,
|
||||||
|
maxBusyTries: 5,
|
||||||
|
recursive: true
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepStrictEqual(validateRmdirOptions(), defaults);
|
||||||
|
assert.deepStrictEqual(validateRmdirOptions({}), defaults);
|
||||||
|
assert.deepStrictEqual(validateRmdirOptions(modified), modified);
|
||||||
|
assert.deepStrictEqual(validateRmdirOptions({
|
||||||
|
maxBusyTries: 99
|
||||||
|
}), {
|
||||||
|
emfileWait: 1000,
|
||||||
|
maxBusyTries: 99,
|
||||||
|
recursive: false
|
||||||
|
});
|
||||||
|
|
||||||
|
[null, 'foo', 5, NaN].forEach((bad) => {
|
||||||
|
common.expectsError(() => {
|
||||||
|
validateRmdirOptions(bad);
|
||||||
|
}, {
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
type: TypeError,
|
||||||
|
message: /^The "options" argument must be of type object\./
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[undefined, null, 'foo', Infinity, function() {}].forEach((bad) => {
|
||||||
|
common.expectsError(() => {
|
||||||
|
validateRmdirOptions({ recursive: bad });
|
||||||
|
}, {
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
type: TypeError,
|
||||||
|
message: /^The "recursive" argument must be of type boolean\./
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
common.expectsError(() => {
|
||||||
|
validateRmdirOptions({ emfileWait: -1 });
|
||||||
|
}, {
|
||||||
|
code: 'ERR_OUT_OF_RANGE',
|
||||||
|
type: RangeError,
|
||||||
|
message: /^The value of "emfileWait" is out of range\./
|
||||||
|
});
|
||||||
|
|
||||||
|
common.expectsError(() => {
|
||||||
|
validateRmdirOptions({ maxBusyTries: -1 });
|
||||||
|
}, {
|
||||||
|
code: 'ERR_OUT_OF_RANGE',
|
||||||
|
type: RangeError,
|
||||||
|
message: /^The value of "maxBusyTries" is out of range\./
|
||||||
|
});
|
||||||
|
}
|
@ -106,4 +106,7 @@ addlicense "HdrHistogram" "deps/histogram" "$(cat ${rootdir}/deps/histogram/LICE
|
|||||||
addlicense "node-heapdump" "src/heap_utils.cc" \
|
addlicense "node-heapdump" "src/heap_utils.cc" \
|
||||||
"$(curl -sL https://raw.githubusercontent.com/bnoordhuis/node-heapdump/0ca52441e46241ffbea56a389e2856ec01c48c97/LICENSE)"
|
"$(curl -sL https://raw.githubusercontent.com/bnoordhuis/node-heapdump/0ca52441e46241ffbea56a389e2856ec01c48c97/LICENSE)"
|
||||||
|
|
||||||
|
addlicense "rimraf" "lib/internal/fs/rimraf.js" \
|
||||||
|
"$(curl -sL https://raw.githubusercontent.com/isaacs/rimraf/0e365ac4e4d64a25aa2a3cc026348f13410210e1/LICENSE)"
|
||||||
|
|
||||||
mv $tmplicense $licensefile
|
mv $tmplicense $licensefile
|
||||||
|
Loading…
x
Reference in New Issue
Block a user