Skip to content

fs: implement fs.rmdir recurisve #28171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions doc/api/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -2927,7 +2927,7 @@ changes:

Synchronous rename(2). Returns `undefined`.

## fs.rmdir(path, callback)
## fs.rmdir(path[, options], callback)
<!-- YAML
added: v0.0.2
changes:
Expand All @@ -2946,6 +2946,8 @@ changes:
-->

* `path` {string|Buffer|URL}
* `options` {Object}
* `clearDir` {boolean} delete non-empty folders **Default:** `false
* `callback` {Function}
* `err` {Error}

Expand All @@ -2955,7 +2957,7 @@ 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.

## fs.rmdirSync(path)
## ## fs.rmdirSync(path[, options])
<!-- YAML
added: v0.1.21
changes:
Expand All @@ -2966,6 +2968,8 @@ changes:
-->

* `path` {string|Buffer|URL}
* `options` {Object}
* `clearDir` {boolean} **Default:** `false`

Synchronous rmdir(2). Returns `undefined`.

Expand Down Expand Up @@ -4464,12 +4468,14 @@ added: v10.0.0
Renames `oldPath` to `newPath` and resolves the `Promise` with no arguments
upon success.

### fsPromises.rmdir(path)
### fsPromises.rmdir(path[, options])
<!-- YAML
added: v10.0.0
-->

* `path` {string|Buffer|URL}
* `options` {Object}
* `clearDir` {boolean} **Default:** `false`
* Returns: {Promise}

Removes the directory identified by `path` then resolves the `Promise` with
Expand Down Expand Up @@ -4963,7 +4969,7 @@ the file contents.
[`fs.readdir()`]: #fs_fs_readdir_path_options_callback
[`fs.readdirSync()`]: #fs_fs_readdirsync_path_options
[`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.symlink()`]: #fs_fs_symlink_target_path_type_callback
[`fs.utimes()`]: #fs_fs_utimes_path_atime_mtime_callback
Expand Down
157 changes: 152 additions & 5 deletions lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -678,21 +678,168 @@ function ftruncateSync(fd, len = 0) {
handleErrorFromBinding(ctx);
}

function rmdir(path, callback) {
callback = makeCallback(callback);
path = getValidatedPath(path);
// Delte All directory
function rmDirAll(path, callback) {
let n = 0;
let errState = null;

function next(err) {
errState = errState || err;
if (--n === 0)
rmEmptyDir(path, callback);
}

function rmFile(path, isDirectory, callback) {
if (isDirectory) {
return _rmdir(path, callback);
}

unlink(path, (err) => {
if (err) {
if (err.code === 'ENOENT')
return callback(null);
if (err.code === 'EPERM')
return _rmdir(path, err, callback);
// Normally it doesn't equal 'EISDIR'
if (err.code === 'EISDIR')
return _rmdir(path, err, callback);
}
return callback(err);
});
}

function _rmdir(path, originalEr, callback) {
if (typeof originalEr === 'function') {
callback = originalEr;
originalEr = null;
}

// Check if it is empty through error.code
rmdir(path, function(err) {
if (err && (err.code === 'ENOTEMPTY' ||
err.code === 'EEXIST' ||
err.code === 'EPERM'))
_rmkids(path, callback);
else if (err && err.code === 'ENOTDIR')
callback(originalEr);
else
callback(err);
});
}

function _rmkids(path, callback) {
readdir(path, { withFileTypes: true }, (err, files) => {
if (err)
return callback(err);

let n = files.length;
if (n === 0)
return rmEmptyDir(path, callback);

let errState;
files.forEach((dirent) => {
const fp = pathModule.join(path, dirent.name);
rmFile(fp, dirent.isDirectory(), (err) => {
if (errState)
return;
if (err)
return callback(errState = err);
if (--n === 0)
return rmEmptyDir(path, callback);
});
});
});
}

readdir(path, { withFileTypes: true }, (err, files) => {
if (err)
return callback(err);

n = files.length;
if (n === 0)
return rmEmptyDir(path, callback);

files.forEach((dirent) => {
const fp = pathModule.join(path, dirent.name);
rmFile(fp, dirent.isDirectory(), (err) => {
if (err && err.code === 'ENOENT')
err = null;
next(err);
});
});
});
}

// Delete empty directory
function rmEmptyDir(path, callback) {
const req = new FSReqCallback();
req.oncomplete = callback;
req.oncomplete = makeCallback(callback);
binding.rmdir(pathModule.toNamespacedPath(path), req);
}

function rmdirSync(path) {
function rmdir(path, options, callback) {
callback = maybeCallback(callback || options);
options = getOptions(options, { clearDir: false });
path = getValidatedPath(path);

const { clearDir } = options;

if (typeof clearDir !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('clearDir', 'boolean', clearDir);

if (clearDir) {
rmDirAll(path, callback);
} else {
rmEmptyDir(path, callback);
}
}

// Delte All directory sync
function rmDirAllSync(path) {
// Non-directory throws an error directly to user
const files = readdirSync(path, { withFileTypes: true });
const n = files.length;

if (n === 0)
return rmEmptyDirSync(path);

for (let i = 0; i < n; i++) {
const dirent = files[i];
const fp = pathModule.join(path, dirent.name);
if (dirent.isDirectory()) {
rmDirAllSync(fp);
} else {
unlinkSync(fp);
}
}

// Try again or more?
rmDirAllSync(path);
}

// Delte empty directory sync
function rmEmptyDirSync(path) {
const ctx = { path };
binding.rmdir(pathModule.toNamespacedPath(path), undefined, ctx);
handleErrorFromBinding(ctx);
}

function rmdirSync(path, options) {
path = getValidatedPath(path);
options = getOptions(options, { clearDir: false });

const { clearDir } = options;

if (typeof clearDir !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('clearDir', 'boolean', clearDir);

if (clearDir) {
rmDirAllSync(path);
} else {
rmEmptyDirSync(path);
}
}

function fdatasync(fd, callback) {
validateUint32(fd, 'fd');
const req = new FSReqCallback();
Expand Down
19 changes: 18 additions & 1 deletion lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ const { kUsePromises } = binding;

const getDirectoryEntriesPromise = promisify(getDirents);

let rmdirPromise;

class FileHandle {
constructor(filehandle) {
this[kHandle] = filehandle;
Expand Down Expand Up @@ -274,8 +276,23 @@ async function ftruncate(handle, len = 0) {
return binding.ftruncate(handle.fd, len, kUsePromises);
}

async function rmdir(path) {
async function rmdir(path, options) {
path = getValidatedPath(path);
options = getOptions(options, { clearDir: false });
const { clearDir } = options;

if (typeof clearDir !== 'boolean')
throw new ERR_INVALID_ARG_TYPE('clearDir', 'boolean', clearDir);

// If implement them in lib, can only import promisify for rmdir.
if (clearDir) {
if (rmdirPromise === undefined) {
const rmdir = require('fs').rmdir;
rmdirPromise = promisify(rmdir);
}
return rmdirPromise(path, options);
}

return binding.rmdir(pathModule.toNamespacedPath(path), kUsePromises);
}

Expand Down
95 changes: 95 additions & 0 deletions test/parallel/test-fs-rmdir-clearDir.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const path = require('path');
const fs = require('fs');
const tmpdir = require('../common/tmpdir');

const tmpPath = (dir) => path.join(tmpdir.path, dir);

tmpdir.refresh();

// fs.rmdir - clearDir: true
{
const paramdir = tmpPath('rmdir');
const d = path.join(paramdir, 'test_rmdir');
// Make sure the directory does not exist
assert(!fs.existsSync(d));
// Create the directory now
fs.mkdirSync(d, { recursive: true });
assert(fs.existsSync(d));
// Create files
fs.writeFileSync(path.join(d, 'test.txt'), 'test');

fs.rmdir(paramdir, { clearDir: true }, common.mustCall((err) => {
assert.ifError(err);
assert(!fs.existsSync(d));
}));
}

// fs.rmdirSync - clearDir: true
{
const paramdir = tmpPath('rmdirSync');
const d = path.join(paramdir, 'test_rmdirSync');
// Make sure the directory does not exist
assert(!fs.existsSync(d));
// Create the directory now
fs.mkdirSync(d, { recursive: true });
assert(fs.existsSync(d));
// Create files
fs.writeFileSync(path.join(d, 'test.txt'), 'test');

fs.rmdirSync(paramdir, { clearDir: true });
assert(!fs.existsSync(d));
}

// fs.promises.rmdir - clearDir: true
{
const paramdir = tmpPath('rmdirPromise');
const d = path.join(paramdir, 'test_promises_rmdir');
// Make sure the directory does not exist
assert(!fs.existsSync(d));
// Create the directory now
fs.mkdirSync(d, { recursive: true });
assert(fs.existsSync(d));
// Create files
fs.writeFileSync(path.join(d, 'test.txt'), 'test');

(async () => {
await fs.promises.rmdir(paramdir, { clearDir: true });
assert(!fs.existsSync(d));
})();
}

// clearDir: false
{
const paramdir = tmpPath('options');
const d = path.join(paramdir, 'dir', 'test_rmdir_recursive_false');
// Make sure the directory does not exist
assert(!fs.existsSync(d));
// Create the directory now
fs.mkdirSync(d, { recursive: true });
assert(fs.existsSync(d));

// fs.rmdir
fs.rmdir(paramdir, { clearDir: false }, common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOTEMPTY');
}));

// fs.rmdirSync
common.expectsError(
() => fs.rmdirSync(paramdir, { clearDir: false }),
{
code: 'ENOTEMPTY'
}
);

// fs.promises.rmdir
assert.rejects(
fs.promises.rmdir(paramdir, { clearDir: false }),
{
code: 'ENOTEMPTY'
}
);
}
Loading