diff --git a/doc/api/fs.md b/doc/api/fs.md index f1b7e1ccbb1c60..71dc234c083014 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -2927,7 +2927,7 @@ changes: Synchronous rename(2). Returns `undefined`. -## fs.rmdir(path, callback) +## fs.rmdir(path[, options], callback) * `path` {string|Buffer|URL} +* `options` {Object} + * `clearDir` {boolean} delete non-empty folders **Default:** `false * `callback` {Function} * `err` {Error} @@ -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]) * `path` {string|Buffer|URL} +* `options` {Object} + * `clearDir` {boolean} **Default:** `false` Synchronous rmdir(2). Returns `undefined`. @@ -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]) * `path` {string|Buffer|URL} +* `options` {Object} + * `clearDir` {boolean} **Default:** `false` * Returns: {Promise} Removes the directory identified by `path` then resolves the `Promise` with @@ -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 diff --git a/lib/fs.js b/lib/fs.js index e890d0c1305b95..6c7fc1383e5b5f 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -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(); diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 42dbbcc361ad99..74a15c576dc769 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -47,6 +47,8 @@ const { kUsePromises } = binding; const getDirectoryEntriesPromise = promisify(getDirents); +let rmdirPromise; + class FileHandle { constructor(filehandle) { this[kHandle] = filehandle; @@ -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); } diff --git a/test/parallel/test-fs-rmdir-clearDir.js b/test/parallel/test-fs-rmdir-clearDir.js new file mode 100644 index 00000000000000..b6952dd03fc838 --- /dev/null +++ b/test/parallel/test-fs-rmdir-clearDir.js @@ -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' + } + ); +} diff --git a/test/parallel/test-fs-rmdir-type-check.js b/test/parallel/test-fs-rmdir-type-check.js index fc2106b8cabee7..8efc0183b714c1 100644 --- a/test/parallel/test-fs-rmdir-type-check.js +++ b/test/parallel/test-fs-rmdir-type-check.js @@ -1,7 +1,12 @@ 'use strict'; const common = require('../common'); +const assert = require('assert'); const fs = require('fs'); +const path = require('path'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); [false, 1, [], {}, null, undefined].forEach((i) => { common.expectsError( @@ -19,3 +24,35 @@ const fs = require('fs'); } ); }); + +const d = path.join(tmpdir.path, 'dir', 'test_rmdir_typecheck'); +// Make sure the directory does not exist +assert(!fs.existsSync(d)); +// Create the directory now +fs.mkdirSync(d, { recursive: true }); + +// Tests for clearDir option +['true', 1, [], {}].forEach((i) => { + common.expectsError( + () => fs.rmdirSync(d, { clearDir: i }), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + } + ); + common.expectsError( + () => fs.rmdir(d, { clearDir: i }, common.mustNotCall()), + { + code: 'ERR_INVALID_ARG_TYPE', + type: TypeError + } + ); + assert.rejects( + fs.promises.rmdir(d, { clearDir: i }), + { + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "clearDir" argument must be of type boolean. ' + + `Received type ${typeof i}` + } + ); +});