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:
cjihrig 2019-08-16 13:17:21 -04:00 committed by Rich Trott
parent 2b1bcba385
commit 53816cce69
9 changed files with 553 additions and 14 deletions

19
LICENSE
View File

@ -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.
"""

View File

@ -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

View File

@ -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);

View File

@ -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
View 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 };

View File

@ -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
}; };

View File

@ -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',

View 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\./
});
}

View File

@ -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