Skip to content

Commit

Permalink
fs: remove permissive rmdir recursive
Browse files Browse the repository at this point in the history
  • Loading branch information
aduh95 committed Mar 19, 2021
1 parent eaf0f0f commit cc39f06
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 39 deletions.
63 changes: 45 additions & 18 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,16 @@ Renames `oldPath` to `newPath`.
<!-- YAML
added: v10.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37216
description: "Using `fsPromises.rmdir(path, { recursive: true })` on a `path`
that is a file is no longer permitted and results in an
`ENOENT` error on Windows and an `ENOTDIR` error on POSIX."
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37216
description: "Using `fsPromises.rmdir(path, { recursive: true })` on a `path`
that does not exist is no longer permitted and results in a
`ENOENT` error."
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37302
description: The `recursive` option is deprecated, using it triggers a
Expand Down Expand Up @@ -1075,8 +1085,8 @@ changes:
represents the number of retries. This option is ignored if the `recursive`
option is not `true`. **Default:** `0`.
* `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`. **Deprecated**.
recursive mode, operations are retried on failure. **Default:** `false`.
**Deprecated**.
* `retryDelay` {integer} The amount of time in milliseconds to wait between
retries. This option is ignored if the `recursive` option is not `true`.
**Default:** `100`.
Expand All @@ -1088,10 +1098,8 @@ Using `fsPromises.rmdir()` on a file (not a directory) results in the
promise being rejected with an `ENOENT` error on Windows and an `ENOTDIR`
error on POSIX.
Setting `recursive` to `true` results in behavior similar to the Unix command
`rm -rf`: an error will not be raised for paths that do not exist, and paths
that represent files will be deleted. The `recursive` option is deprecated,
`ENOTDIR` and `ENOENT` will be thrown in the future.
To get a behavior similar to the `rm -rf` Unix command, use
[`fsPromises.rm()`][] with options `{ recursive: true, force: true }`.
### `fsPromises.rm(path[, options])`
<!-- YAML
Expand Down Expand Up @@ -3146,6 +3154,16 @@ rename('oldFile.txt', 'newFile.txt', (err) => {
<!-- YAML
added: v0.0.2
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37216
description: "Using `fs.rmdir(path, { recursive: true })` on a `path` that is
a file is no longer permitted and results in an `ENOENT` error
on Windows and an `ENOTDIR` error on POSIX."
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37216
description: "Using `fs.rmdir(path, { recursive: true })` on a `path` that
does not exist is no longer permitted and results in a `ENOENT`
error."
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37302
description: The `recursive` option is deprecated, using it triggers a
Expand Down Expand Up @@ -3185,8 +3203,8 @@ changes:
represents the number of retries. This option is ignored if the `recursive`
option is not `true`. **Default:** `0`.
* `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`. **Deprecated**.
recursive mode, operations are retried on failure. **Default:** `false`.
**Deprecated**.
* `retryDelay` {integer} The amount of time in milliseconds to wait between
retries. This option is ignored if the `recursive` option is not `true`.
**Default:** `100`.
Expand All @@ -3199,10 +3217,8 @@ to the completion callback.
Using `fs.rmdir()` on a file (not a directory) results in an `ENOENT` error on
Windows and an `ENOTDIR` error on POSIX.

Setting `recursive` to `true` results in behavior similar to the Unix command
`rm -rf`: an error will not be raised for paths that do not exist, and paths
that represent files will be deleted. The `recursive` option is deprecated,
`ENOTDIR` and `ENOENT` will be thrown in the future.
To get a behavior similar to the `rm -rf` Unix command, use [`fs.rm()`][]
with options `{ recursive: true, force: true }`.

### `fs.rm(path[, options], callback)`
<!-- YAML
Expand Down Expand Up @@ -4777,6 +4793,16 @@ See the POSIX rename(2) documentation for more details.
<!-- YAML
added: v0.1.21
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37216
description: "Using `fs.rmdirSync(path, { recursive: true })` on a `path`
that is a file is no longer permitted and results in an
`ENOENT` error on Windows and an `ENOTDIR` error on POSIX."
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37216
description: "Using `fs.rmdirSync(path, { recursive: true })` on a `path`
that does not exist is no longer permitted and results in a
`ENOENT` error."
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/37302
description: The `recursive` option is deprecated, using it triggers a
Expand Down Expand Up @@ -4808,8 +4834,8 @@ changes:
represents the number of retries. This option is ignored if the `recursive`
option is not `true`. **Default:** `0`.
* `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`. **Deprecated**.
recursive mode, operations are retried on failure. **Default:** `false`.
**Deprecated**.
* `retryDelay` {integer} The amount of time in milliseconds to wait between
retries. This option is ignored if the `recursive` option is not `true`.
**Default:** `100`.
Expand All @@ -4819,10 +4845,8 @@ Synchronous rmdir(2). Returns `undefined`.
Using `fs.rmdirSync()` on a file (not a directory) results in an `ENOENT` error
on Windows and an `ENOTDIR` error on POSIX.
Setting `recursive` to `true` results in behavior similar to the Unix command
`rm -rf`: an error will not be raised for paths that do not exist, and paths
that represent files will be deleted. The `recursive` option is deprecated,
`ENOTDIR` and `ENOENT` will be thrown in the future.
To get a behavior similar to the `rm -rf` Unix command, use [`fs.rmSync()`][]
with options `{ recursive: true, force: true }`.
### `fs.rmSync(path[, options])`
<!-- YAML
Expand Down Expand Up @@ -6670,6 +6694,8 @@ the file contents.
[`fs.readdirSync()`]: #fs_fs_readdirsync_path_options
[`fs.readv()`]: #fs_fs_readv_fd_buffers_position_callback
[`fs.realpath()`]: #fs_fs_realpath_path_options_callback
[`fs.rm()`]: #fs_fs_rm_path_options_callback
[`fs.rmSync()`]: #fs_fs_rmsync_path_options
[`fs.rmdir()`]: #fs_fs_rmdir_path_options_callback
[`fs.stat()`]: #fs_fs_stat_path_options_callback
[`fs.symlink()`]: #fs_fs_symlink_target_path_type_callback
Expand All @@ -6681,6 +6707,7 @@ the file contents.
[`fs.writev()`]: #fs_fs_writev_fd_buffers_position_callback
[`fsPromises.open()`]: #fs_fspromises_open_path_flags_mode
[`fsPromises.opendir()`]: #fs_fspromises_opendir_path_options
[`fsPromises.rm()`]: #fs_fspromises_rm_path_options
[`fsPromises.utimes()`]: #fs_fspromises_utimes_path_atime_mtime
[`inotify(7)`]: https://man7.org/linux/man-pages/man7/inotify.7.html
[`kqueue(2)`]: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
Expand Down
20 changes: 14 additions & 6 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -898,15 +898,20 @@ function rmdir(path, options, callback) {
emitRecursiveRmdirWarning();
validateRmOptions(
path,
{ ...options, force: true },
{ ...options, force: false },
true,
(err, options) => {
if (err === false) {
const req = new FSReqCallback();
req.oncomplete = callback;
return binding.rmdir(path, req);
}
if (err) {
return callback(err);
}

lazyLoadRimraf();
return rimraf(path, options, callback);
rimraf(path, options, callback);
});
} else {
validateRmdirOptions(options);
Expand All @@ -921,12 +926,15 @@ function rmdirSync(path, options) {

if (options?.recursive) {
emitRecursiveRmdirWarning();
options = validateRmOptionsSync(path, { ...options, force: true }, true);
lazyLoadRimraf();
return rimrafSync(pathModule.toNamespacedPath(path), options);
options = validateRmOptionsSync(path, { ...options, force: false }, true);
if (options !== false) {
lazyLoadRimraf();
return rimrafSync(pathModule.toNamespacedPath(path), options);
}
} else {
validateRmdirOptions(options);
}

validateRmdirOptions(options);
const ctx = { path };
binding.rmdir(pathModule.toNamespacedPath(path), undefined, ctx);
return handleErrorFromBinding(ctx);
Expand Down
5 changes: 4 additions & 1 deletion lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,10 @@ async function rmdir(path, options) {

if (options.recursive) {
emitRecursiveRmdirWarning();
return rimrafPromises(path, options);
const stats = await stat(path);
if (stats.isDirectory()) {
return rimrafPromises(path, options);
}
}

return binding.rmdir(path, kUsePromises);
Expand Down
22 changes: 15 additions & 7 deletions lib/internal/fs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -698,39 +698,47 @@ const defaultRmdirOptions = {
recursive: false,
};

const validateRmOptions = hideStackFrames((path, options, warn, callback) => {
const validateRmOptions = hideStackFrames((path, options, expectDir, cb) => {
options = validateRmdirOptions(options, defaultRmOptions);
validateBoolean(options.force, 'options.force');

lazyLoadFs().stat(path, (err, stats) => {
if (err) {
if (options.force && err.code === 'ENOENT') {
return callback(null, options);
return cb(null, options);
}
return callback(err, options);
return cb(err, options);
}

if (expectDir && !stats.isDirectory()) {
return cb(false);
}

if (stats.isDirectory() && !options.recursive) {
return callback(new ERR_FS_EISDIR({
return cb(new ERR_FS_EISDIR({
code: 'EISDIR',
message: 'is a directory',
path,
syscall: 'rm',
errno: EISDIR
}));
}
return callback(null, options);
return cb(null, options);
});
});

const validateRmOptionsSync = hideStackFrames((path, options, warn) => {
const validateRmOptionsSync = hideStackFrames((path, options, expectDir) => {
options = validateRmdirOptions(options, defaultRmOptions);
validateBoolean(options.force, 'options.force');

if (!options.force || warn || !options.recursive) {
if (!options.force || expectDir || !options.recursive) {
const isDirectory = lazyLoadFs()
.statSync(path, { throwIfNoEntry: !options.force })?.isDirectory();

if (expectDir && !isDirectory) {
return false;
}

if (isDirectory && !options.recursive) {
throw new ERR_FS_EISDIR({
code: 'EISDIR',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');

Expand All @@ -14,5 +15,11 @@ tmpdir.refresh();
'will be removed. Use fs.rm(path, { recursive: true }) instead',
'DEP0147'
);
fs.rmdirSync(path.join(tmpdir.path, 'noexist.txt'), { recursive: true });
assert.throws(
() => fs.rmdirSync(path.join(tmpdir.path, 'noexist.txt'),
{ recursive: true }),
{
name: 'ERR'
}
);
}
36 changes: 36 additions & 0 deletions test/parallel/test-fs-rmdir-recursive-throws-not-found.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');

tmpdir.refresh();

{
assert.throws(
() =>
fs.rmdirSync(path.join(tmpdir.path, 'noexist.txt'), { recursive: true }),
{
code: 'ENOENT',
}
);
}
{
fs.rmdir(
path.join(tmpdir.path, 'noexist.txt'),
{ recursive: true },
common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
})
);
}
{
assert.rejects(
() => fs.promises.rmdir(path.join(tmpdir.path, 'noexist.txt'),
{ recursive: true }),
{
code: 'ENOENT',
}
).then(common.mustCall());
}
30 changes: 30 additions & 0 deletions test/parallel/test-fs-rmdir-recursive-throws-on-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const assert = require('assert');
const fs = require('fs');
const path = require('path');

tmpdir.refresh();

{
const filePath = path.join(tmpdir.path, 'rmdir-recursive.txt');
fs.writeFileSync(filePath, '');
assert.throws(() => fs.rmdirSync(filePath, { recursive: true }), {
code: 'ENOTDIR',
});
}
{
const filePath = path.join(tmpdir.path, 'rmdir-recursive.txt');
fs.writeFileSync(filePath, '');
fs.rmdir(filePath, { recursive: true }, common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOTDIR');
}));
}
{
const filePath = path.join(tmpdir.path, 'rmdir-recursive.txt');
fs.writeFileSync(filePath, '');
assert.rejects(() => fs.promises.rmdir(filePath, { recursive: true }), {
code: 'ENOTDIR',
}).then(common.mustCall());
}
15 changes: 9 additions & 6 deletions test/parallel/test-fs-rmdir-recursive.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ function removeAsync(dir) {

// Recursive removal should succeed.
fs.rmdir(dir, { recursive: true }, common.mustSucceed(() => {
// No error should occur if recursive and the directory does not exist.
fs.rmdir(dir, { recursive: true }, common.mustSucceed(() => {
// An error should occur if recursive and the directory does not exist.
fs.rmdir(dir, { recursive: true }, common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOENT');
// Attempted removal should fail now because the directory is gone.
fs.rmdir(dir, common.mustCall((err) => {
assert.strictEqual(err.syscall, 'rmdir');
Expand Down Expand Up @@ -126,8 +127,9 @@ function removeAsync(dir) {
// 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 });
// An error should occur if recursive and the directory does not exist.
assert.throws(() => fs.rmdirSync(dir, { recursive: true }),
{ code: 'ENOENT' });

// Attempted removal should fail now because the directory is gone.
assert.throws(() => fs.rmdirSync(dir), { syscall: 'rmdir' });
Expand All @@ -147,8 +149,9 @@ function removeAsync(dir) {
// 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 });
// An error should occur if recursive and the directory does not exist.
await assert.rejects(fs.promises.rmdir(dir, { recursive: true }),
{ code: 'ENOENT' });

// Attempted removal should fail now because the directory is gone.
assert.rejects(fs.promises.rmdir(dir), { syscall: 'rmdir' });
Expand Down

0 comments on commit cc39f06

Please sign in to comment.