Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE19
-rw-r--r--doc/api/errors.md69
-rw-r--r--doc/api/fs.md91
-rw-r--r--lib/fs.js50
-rw-r--r--lib/internal/errors.js11
-rw-r--r--lib/internal/fs/cp/cp-sync.js331
-rw-r--r--lib/internal/fs/cp/cp.js384
-rw-r--r--lib/internal/fs/promises.js16
-rw-r--r--lib/internal/fs/utils.js27
-rw-r--r--test/fixtures/copy/kitchen-sink/README.md1
-rw-r--r--test/fixtures/copy/kitchen-sink/a/b/README2.md1
-rw-r--r--test/fixtures/copy/kitchen-sink/a/b/index.js3
-rw-r--r--test/fixtures/copy/kitchen-sink/a/c/README2.md1
-rw-r--r--test/fixtures/copy/kitchen-sink/a/c/d/README3.md1
-rw-r--r--test/fixtures/copy/kitchen-sink/a/c/d/index.js3
-rw-r--r--test/fixtures/copy/kitchen-sink/a/c/index.js3
-rw-r--r--test/fixtures/copy/kitchen-sink/a/index.js3
-rw-r--r--test/fixtures/copy/kitchen-sink/index.js3
-rw-r--r--test/parallel/test-fs-cp.mjs763
-rwxr-xr-xtools/license-builder.sh3
20 files changed, 1782 insertions, 1 deletions
diff --git a/LICENSE b/LICENSE
index 82a9eafafe5..29bd53a3610 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1584,3 +1584,22 @@ The externally maintained libraries used by Node.js are:
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
+
+- node-fs-extra, located at lib/internal/fs/cp, is licensed as follows:
+ """
+ (The MIT License)
+
+ Copyright (c) 2011-2017 JP Richardson
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
+ (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
+ merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ """
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 9b1a556d2b0..67dc1781d0b 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -1115,6 +1115,74 @@ added: v14.0.0
Used when a feature that is not available
to the current platform which is running Node.js is used.
+<a id="ERR_FS_CP_DIR_TO_NON_DIR"></a>
+### `ERR_FS_CP_DIR_TO_NON_DIR`
+<!--
+added: REPLACEME
+-->
+
+An attempt was made to copy a directory to a non-directory (file, symlink,
+etc.) using [`fs.cp()`][].
+
+<a id="ERR_FS_CP_EEXIST"></a>
+### `ERR_FS_CP_EEXIST`
+<!--
+added: REPLACEME
+-->
+
+An attempt was made to copy over a file that already existed with
+[`fs.cp()`][], with the `force` and `errorOnExist` set to `true`.
+
+<a id="ERR_FS_CP_EINVAL"></a>
+### `ERR_FS_CP_EINVAL`
+<!--
+added: REPLACEME
+-->
+
+When using [`fs.cp()`][], `src` or `dest` pointed to an invalid path.
+
+<a id="ERR_FS_CP_FIFO_PIPE"></a>
+### `ERR_FS_CP_FIFO_PIPE`
+<!--
+added: REPLACEME
+-->
+
+An attempt was made to copy a named pipe with [`fs.cp()`][].
+
+<a id="ERR_FS_CP_NON_DIR_TO_DIR"></a>
+### `ERR_FS_CP_NON_DIR_TO_DIR`
+<!--
+added: REPLACEME
+-->
+
+An attempt was made to copy a non-directory (file, symlink, etc.) to a directory
+using [`fs.cp()`][].
+
+<a id="ERR_FS_CP_SOCKET"></a>
+### `ERR_FS_CP_SOCKET`
+<!--
+added: REPLACEME
+-->
+
+An attempt was made to copy to a socket with [`fs.cp()`][].
+
+<a id="ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY"></a>
+### `ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY`
+<!--
+added: REPLACEME
+-->
+
+When using [`fs.cp()`][], a symlink in `dest` pointed to a subdirectory
+of `src`.
+
+<a id="ERR_FS_CP_UNKNOWN"></a>
+### `ERR_FS_CP_UNKNOWN`
+<!--
+added: REPLACEME
+-->
+
+An attempt was made to copy to an unknown file type with [`fs.cp()`][].
+
<a id="ERR_FS_EISDIR"></a>
### `ERR_FS_EISDIR`
@@ -2822,6 +2890,7 @@ The native call from `process.cpuUsage` could not be processed.
[`dgram.remoteAddress()`]: dgram.md#dgram_socket_remoteaddress
[`errno`(3) man page]: https://man7.org/linux/man-pages/man3/errno.3.html
[`fs.Dir`]: fs.md#fs_class_fs_dir
+[`fs.cp()`]: fs.md#fs_fs_cp_src_dest_options_callback
[`fs.readFileSync`]: fs.md#fs_fs_readfilesync_path_options
[`fs.readdir`]: fs.md#fs_fs_readdir_path_options_callback
[`fs.symlink()`]: fs.md#fs_fs_symlink_target_path_type_callback
diff --git a/doc/api/fs.md b/doc/api/fs.md
index f17f0559753..a98a937c411 100644
--- a/doc/api/fs.md
+++ b/doc/api/fs.md
@@ -739,6 +739,37 @@ try {
}
```
+### `fsPromises.cp(src, dest[, options])`
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `src` {string|URL} source path to copy.
+* `dest` {string|URL} destination path to copy to.
+* `options` {Object}
+ * `dereference` {boolean} dereference symlinks. **Default:** `false`.
+ * `errorOnExist` {boolean} when `force` is `false`, and the destination
+ exists, throw an error. **Default:** `false`.
+ * `filter` {Function} Function to filter copied files/directories. Return
+ `true` to copy the item, `false` to ignore it. Can also return a `Promise`
+ that resolves to `true` or `false` **Default:** `undefined`.
+ * `force` {boolean} overwrite existing file or directory. _The copy
+ operation will ignore errors if you set this to false and the destination
+ exists. Use the `errorOnExist` option to change this behavior.
+ **Default:** `true`.
+ * `preserveTimestamps` {boolean} When `true` timestamps from `src` will
+ be preserved. **Default:** `false`.
+ * `recursive` {boolean} copy directories recursively **Default:** `false`
+* Returns: {Promise} Fulfills with `undefined` upon success.
+
+Asynchronously copies the entire directory structure from `src` to `dest`,
+including subdirectories and files.
+
+When copying a directory to another directory, globs are not supported and
+behavior is similar to `cp dir1/ dir2/`.
+
### `fsPromises.lchmod(path, mode)`
<!-- YAML
deprecated: v10.0.0
@@ -1848,6 +1879,37 @@ copyFile('source.txt', 'destination.txt', callback);
copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback);
```
+### `fs.cp(src, dest[, options], callback)`
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `src` {string|URL} source path to copy.
+* `dest` {string|URL} destination path to copy to.
+* `options` {Object}
+ * `dereference` {boolean} dereference symlinks. **Default:** `false`.
+ * `errorOnExist` {boolean} when `force` is `false`, and the destination
+ exists, throw an error. **Default:** `false`.
+ * `filter` {Function} Function to filter copied files/directories. Return
+ `true` to copy the item, `false` to ignore it. Can also return a `Promise`
+ that resolves to `true` or `false` **Default:** `undefined`.
+ * `force` {boolean} overwrite existing file or directory. _The copy
+ operation will ignore errors if you set this to false and the destination
+ exists. Use the `errorOnExist` option to change this behavior.
+ **Default:** `true`.
+ * `preserveTimestamps` {boolean} When `true` timestamps from `src` will
+ be preserved. **Default:** `false`.
+ * `recursive` {boolean} copy directories recursively **Default:** `false`
+* `callback` {Function}
+
+Asynchronously copies the entire directory structure from `src` to `dest`,
+including subdirectories and files.
+
+When copying a directory to another directory, globs are not supported and
+behavior is similar to `cp dir1/ dir2/`.
+
### `fs.createReadStream(path[, options])`
<!-- YAML
added: v0.1.31
@@ -4321,6 +4383,35 @@ console.log('source.txt was copied to destination.txt');
copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL);
```
+### `fs.cpSync(src, dest[, options])`
+<!-- YAML
+added: REPLACEME
+-->
+
+> Stability: 1 - Experimental
+
+* `src` {string|URL} source path to copy.
+* `dest` {string|URL} destination path to copy to.
+* `options` {Object}
+ * `dereference` {boolean} dereference symlinks. **Default:** `false`.
+ * `errorOnExist` {boolean} when `force` is `false`, and the destination
+ exists, throw an error. **Default:** `false`.
+ * `filter` {Function} Function to filter copied files/directories. Return
+ `true` to copy the item, `false` to ignore it. **Default:** `undefined`
+ * `force` {boolean} overwrite existing file or directory. _The copy
+ operation will ignore errors if you set this to false and the destination
+ exists. Use the `errorOnExist` option to change this behavior.
+ **Default:** `true`.
+ * `preserveTimestamps` {boolean} When `true` timestamps from `src` will
+ be preserved. **Default:** `false`.
+ * `recursive` {boolean} copy directories recursively **Default:** `false`
+
+Synchronously copies the entire directory structure from `src` to `dest`,
+including subdirectories and files.
+
+When copying a directory to another directory, globs are not supported and
+behavior is similar to `cp dir1/ dir2/`.
+
### `fs.existsSync(path)`
<!-- YAML
added: v0.1.21
diff --git a/lib/fs.js b/lib/fs.js
index cf3c885b31c..7e126b84ade 100644
--- a/lib/fs.js
+++ b/lib/fs.js
@@ -107,6 +107,7 @@ const {
stringToSymlinkType,
toUnixTimestamp,
validateBufferArray,
+ validateCpOptions,
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
@@ -145,6 +146,8 @@ let truncateWarn = true;
let fs;
// Lazy loaded
+let cpFn;
+let cpSyncFn;
let promises = null;
let ReadStream;
let WriteStream;
@@ -1075,6 +1078,13 @@ function ftruncateSync(fd, len = 0) {
handleErrorFromBinding(ctx);
}
+function lazyLoadCp() {
+ if (cpFn === undefined) {
+ ({ cpFn } = require('internal/fs/cp/cp'));
+ cpFn = require('util').callbackify(cpFn);
+ ({ cpSyncFn } = require('internal/fs/cp/cp-sync'));
+ }
+}
function lazyLoadRimraf() {
if (rimraf === undefined)
@@ -2790,6 +2800,44 @@ function copyFileSync(src, dest, mode) {
handleErrorFromBinding(ctx);
}
+/**
+ * Asynchronously copies `src` to `dest`. `src` can be a file, directory, or
+ * symlink. The contents of directories will be copied recursively.
+ * @param {string | URL} src
+ * @param {string | URL} dest
+ * @param {Object} [options]
+ * @param {() => any} callback
+ * @returns {void}
+ */
+function cp(src, dest, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+ callback = makeCallback(callback);
+ options = validateCpOptions(options);
+ src = pathModule.toNamespacedPath(getValidatedPath(src, 'src'));
+ dest = pathModule.toNamespacedPath(getValidatedPath(dest, 'dest'));
+ lazyLoadCp();
+ cpFn(src, dest, options, callback);
+}
+
+/**
+ * Synchronously copies `src` to `dest`. `src` can be a file, directory, or
+ * symlink. The contents of directories will be copied recursively.
+ * @param {string | URL} src
+ * @param {string | URL} dest
+ * @param {Object} [options]
+ * @returns {void}
+ */
+function cpSync(src, dest, options) {
+ options = validateCpOptions(options);
+ src = pathModule.toNamespacedPath(getValidatedPath(src, 'src'));
+ dest = pathModule.toNamespacedPath(getValidatedPath(dest, 'dest'));
+ lazyLoadCp();
+ cpSyncFn(src, dest, options);
+}
+
function lazyLoadStreams() {
if (!ReadStream) {
({ ReadStream, WriteStream } = require('internal/fs/streams'));
@@ -2854,6 +2902,8 @@ module.exports = fs = {
closeSync,
copyFile,
copyFileSync,
+ cp,
+ cpSync,
createReadStream,
createWriteStream,
exists,
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 25b847b140a..7541d7d1eb8 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -961,6 +961,17 @@ E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
'The feature %s is unavailable on the current platform' +
', which is being used to run Node.js',
TypeError);
+E('ERR_FS_CP_DIR_TO_NON_DIR',
+ 'Cannot overwrite directory with non-directory', SystemError);
+E('ERR_FS_CP_EEXIST', 'Target already exists', SystemError);
+E('ERR_FS_CP_EINVAL', 'Invalid src or dest', SystemError);
+E('ERR_FS_CP_FIFO_PIPE', 'Cannot copy a FIFO pipe', SystemError);
+E('ERR_FS_CP_NON_DIR_TO_DIR',
+ 'Cannot overwrite non-directory with directory', SystemError);
+E('ERR_FS_CP_SOCKET', 'Cannot copy a socket file', SystemError);
+E('ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY',
+ 'Cannot overwrite symlink in subdirectory of self', SystemError);
+E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type', SystemError);
E('ERR_FS_EISDIR', 'Path is a directory', SystemError);
E('ERR_FS_FILE_TOO_LARGE', 'File size (%s) is greater than 2 GB', RangeError);
E('ERR_FS_INVALID_SYMLINK_TYPE',
diff --git a/lib/internal/fs/cp/cp-sync.js b/lib/internal/fs/cp/cp-sync.js
new file mode 100644
index 00000000000..ca2102aeb5a
--- /dev/null
+++ b/lib/internal/fs/cp/cp-sync.js
@@ -0,0 +1,331 @@
+'use strict';
+
+// This file is a modified version of the fs-extra's copySync method.
+
+const { areIdentical, isSrcSubdir } = require('internal/fs/cp/cp');
+const { codes } = require('internal/errors');
+const {
+ os: {
+ errno: {
+ EEXIST,
+ EISDIR,
+ EINVAL,
+ ENOTDIR,
+ }
+ }
+} = internalBinding('constants');
+const {
+ ERR_FS_CP_DIR_TO_NON_DIR,
+ ERR_FS_CP_EEXIST,
+ ERR_FS_CP_EINVAL,
+ ERR_FS_CP_FIFO_PIPE,
+ ERR_FS_CP_NON_DIR_TO_DIR,
+ ERR_FS_CP_SOCKET,
+ ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY,
+ ERR_FS_CP_UNKNOWN,
+ ERR_FS_EISDIR,
+ ERR_INVALID_RETURN_VALUE,
+} = codes;
+const fs = require('fs');
+const {
+ chmodSync,
+ copyFileSync,
+ existsSync,
+ lstatSync,
+ mkdirSync,
+ readdirSync,
+ readlinkSync,
+ statSync,
+ symlinkSync,
+ unlinkSync,
+ utimesSync,
+} = fs;
+const path = require('path');
+const {
+ dirname,
+ isAbsolute,
+ join,
+ parse,
+ resolve,
+} = path;
+const { isPromise } = require('util/types');
+
+function cpSyncFn(src, dest, opts) {
+ // Warn about using preserveTimestamps on 32-bit node
+ if (opts.preserveTimestamps && process.arch === 'ia32') {
+ const warning = 'Using the preserveTimestamps option in 32-bit ' +
+ 'node is not recommended';
+ process.emitWarning(warning, 'TimestampPrecisionWarning');
+ }
+ const { srcStat, destStat } = checkPathsSync(src, dest, opts);
+ checkParentPathsSync(src, srcStat, dest);
+ return handleFilterAndCopy(destStat, src, dest, opts);
+}
+
+function checkPathsSync(src, dest, opts) {
+ const { srcStat, destStat } = getStatsSync(src, dest, opts);
+
+ if (destStat) {
+ if (areIdentical(srcStat, destStat)) {
+ throw new ERR_FS_CP_EINVAL({
+ message: 'src and dest cannot be the same',
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ if (srcStat.isDirectory() && !destStat.isDirectory()) {
+ throw new ERR_FS_CP_DIR_TO_NON_DIR({
+ message: `cannot overwrite directory ${src} ` +
+ `with non-directory ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EISDIR,
+ });
+ }
+ if (!srcStat.isDirectory() && destStat.isDirectory()) {
+ throw new ERR_FS_CP_NON_DIR_TO_DIR({
+ message: `cannot overwrite non-directory ${src} ` +
+ `with directory ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: ENOTDIR,
+ });
+ }
+ }
+
+ if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
+ throw new ERR_FS_CP_EINVAL({
+ message: `cannot copy ${src} to a subdirectory of self ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ return { srcStat, destStat };
+}
+
+function getStatsSync(src, dest, opts) {
+ let destStat;
+ const statFunc = opts.dereference ?
+ (file) => statSync(file, { bigint: true }) :
+ (file) => lstatSync(file, { bigint: true });
+ const srcStat = statFunc(src);
+ try {
+ destStat = statFunc(dest);
+ } catch (err) {
+ if (err.code === 'ENOENT') return { srcStat, destStat: null };
+ throw err;
+ }
+ return { srcStat, destStat };
+}
+
+function checkParentPathsSync(src, srcStat, dest) {
+ const srcParent = resolve(dirname(src));
+ const destParent = resolve(dirname(dest));
+ if (destParent === srcParent || destParent === parse(destParent).root) return;
+ let destStat;
+ try {
+ destStat = statSync(destParent, { bigint: true });
+ } catch (err) {
+ if (err.code === 'ENOENT') return;
+ throw err;
+ }
+ if (areIdentical(srcStat, destStat)) {
+ throw new ERR_FS_CP_EINVAL({
+ message: `cannot copy ${src} to a subdirectory of self ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ return checkParentPathsSync(src, srcStat, destParent);
+}
+
+function handleFilterAndCopy(destStat, src, dest, opts) {
+ if (opts.filter) {
+ const shouldCopy = opts.filter(src, dest);
+ if (isPromise(shouldCopy)) {
+ throw new ERR_INVALID_RETURN_VALUE('boolean', 'filter', shouldCopy);
+ }
+ if (!shouldCopy) return;
+ }
+ const destParent = dirname(dest);
+ if (!existsSync(destParent)) mkdirSync(destParent, { recursive: true });
+ return getStats(destStat, src, dest, opts);
+}
+
+function startCopy(destStat, src, dest, opts) {
+ if (opts.filter && !opts.filter(src, dest)) return;
+ return getStats(destStat, src, dest, opts);
+}
+
+function getStats(destStat, src, dest, opts) {
+ const statSyncFn = opts.dereference ? statSync : lstatSync;
+ const srcStat = statSyncFn(src);
+
+ if (srcStat.isDirectory() && opts.recursive) {
+ return onDir(srcStat, destStat, src, dest, opts);
+ } else if (srcStat.isDirectory()) {
+ throw new ERR_FS_EISDIR({
+ message: `${src} is a directory (not copied)`,
+ path: src,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ } else if (srcStat.isFile() ||
+ srcStat.isCharacterDevice() ||
+ srcStat.isBlockDevice()) {
+ return onFile(srcStat, destStat, src, dest, opts);
+ } else if (srcStat.isSymbolicLink()) {
+ return onLink(destStat, src, dest);
+ } else if (srcStat.isSocket()) {
+ throw new ERR_FS_CP_SOCKET({
+ message: `cannot copy a socket file: ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ } else if (srcStat.isFIFO()) {
+ throw new ERR_FS_CP_FIFO_PIPE({
+ message: `cannot copy a FIFO pipe: ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ throw new ERR_FS_CP_UNKNOWN({
+ message: `cannot copy an unknown file type: ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+}
+
+function onFile(srcStat, destStat, src, dest, opts) {
+ if (!destStat) return copyFile(srcStat, src, dest, opts);
+ return mayCopyFile(srcStat, src, dest, opts);
+}
+
+function mayCopyFile(srcStat, src, dest, opts) {
+ if (opts.force) {
+ unlinkSync(dest);
+ return copyFile(srcStat, src, dest, opts);
+ } else if (opts.errorOnExist) {
+ throw new ERR_FS_CP_EEXIST({
+ message: `${dest} already exists`,
+ path: dest,
+ syscall: 'cp',
+ errno: EEXIST,
+ });
+ }
+}
+
+function copyFile(srcStat, src, dest, opts) {
+ copyFileSync(src, dest);
+ if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest);
+ return setDestMode(dest, srcStat.mode);
+}
+
+function handleTimestamps(srcMode, src, dest) {
+ // Make sure the file is writable before setting the timestamp
+ // otherwise open fails with EPERM when invoked with 'r+'
+ // (through utimes call)
+ if (fileIsNotWritable(srcMode)) makeFileWritable(dest, srcMode);
+ return setDestTimestamps(src, dest);
+}
+
+function fileIsNotWritable(srcMode) {
+ return (srcMode & 0o200) === 0;
+}
+
+function makeFileWritable(dest, srcMode) {
+ return setDestMode(dest, srcMode | 0o200);
+}
+
+function setDestMode(dest, srcMode) {
+ return chmodSync(dest, srcMode);
+}
+
+function setDestTimestamps(src, dest) {
+ // The initial srcStat.atime cannot be trusted
+ // because it is modified by the read(2) system call
+ // (See https://nodejs.org/api/fs.html#fs_stat_time_values)
+ const updatedSrcStat = statSync(src);
+ return utimesSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime);
+}
+
+function onDir(srcStat, destStat, src, dest, opts) {
+ if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts);
+ return copyDir(src, dest, opts);
+}
+
+function mkDirAndCopy(srcMode, src, dest, opts) {
+ mkdirSync(dest);
+ copyDir(src, dest, opts);
+ return setDestMode(dest, srcMode);
+}
+
+function copyDir(src, dest, opts) {
+ readdirSync(src).forEach((item) => copyDirItem(item, src, dest, opts));
+}
+
+function copyDirItem(item, src, dest, opts) {
+ const srcItem = join(src, item);
+ const destItem = join(dest, item);
+ const { destStat } = checkPathsSync(srcItem, destItem, opts);
+ return startCopy(destStat, srcItem, destItem, opts);
+}
+
+function onLink(destStat, src, dest) {
+ let resolvedSrc = readlinkSync(src);
+ if (!isAbsolute(resolvedSrc)) {
+ resolvedSrc = resolve(dirname(src), resolvedSrc);
+ }
+ if (!destStat) {
+ return symlinkSync(resolvedSrc, dest);
+ }
+ let resolvedDest;
+ try {
+ resolvedDest = readlinkSync(dest);
+ } catch (err) {
+ // Dest exists and is a regular file or directory,
+ // Windows may throw UNKNOWN error. If dest already exists,
+ // fs throws error anyway, so no need to guard against it here.
+ if (err.code === 'EINVAL' || err.code === 'UNKNOWN') {
+ return symlinkSync(resolvedSrc, dest);
+ }
+ throw err;
+ }
+ if (!isAbsolute(resolvedDest)) {
+ resolvedDest = resolve(dirname(dest), resolvedDest);
+ }
+ if (isSrcSubdir(resolvedSrc, resolvedDest)) {
+ throw new ERR_FS_CP_EINVAL({
+ message: `cannot copy ${resolvedSrc} to a subdirectory of self ` +
+ `${resolvedDest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ // Prevent copy if src is a subdir of dest since unlinking
+ // dest in this case would result in removing src contents
+ // and therefore a broken symlink would be created.
+ if (statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
+ throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({
+ message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ return copyLink(resolvedSrc, dest);
+}
+
+function copyLink(resolvedSrc, dest) {
+ unlinkSync(dest);
+ return symlinkSync(resolvedSrc, dest);
+}
+
+module.exports = { cpSyncFn };
diff --git a/lib/internal/fs/cp/cp.js b/lib/internal/fs/cp/cp.js
new file mode 100644
index 00000000000..9ee661be68d
--- /dev/null
+++ b/lib/internal/fs/cp/cp.js
@@ -0,0 +1,384 @@
+'use strict';
+
+// This file is a modified version of the fs-extra's copy method.
+
+const {
+ ArrayPrototypeEvery,
+ ArrayPrototypeFilter,
+ Boolean,
+ PromiseAll,
+ PromisePrototypeCatch,
+ PromisePrototypeThen,
+ PromiseReject,
+ SafeArrayIterator,
+ StringPrototypeSplit,
+} = primordials;
+const {
+ codes: {
+ ERR_FS_CP_DIR_TO_NON_DIR,
+ ERR_FS_CP_EEXIST,
+ ERR_FS_CP_EINVAL,
+ ERR_FS_CP_FIFO_PIPE,
+ ERR_FS_CP_NON_DIR_TO_DIR,
+ ERR_FS_CP_SOCKET,
+ ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY,
+ ERR_FS_CP_UNKNOWN,
+ ERR_FS_EISDIR,
+ },
+} = require('internal/errors');
+const {
+ os: {
+ errno: {
+ EEXIST,
+ EISDIR,
+ EINVAL,
+ ENOTDIR,
+ }
+ }
+} = internalBinding('constants');
+const {
+ chmod,
+ copyFile,
+ lstat,
+ mkdir,
+ readdir,
+ readlink,
+ stat,
+ symlink,
+ unlink,
+ utimes,
+} = require('fs/promises');
+const {
+ dirname,
+ isAbsolute,
+ join,
+ parse,
+ resolve,
+ sep,
+} = require('path');
+
+async function cpFn(src, dest, opts) {
+ // Warn about using preserveTimestamps on 32-bit node
+ if (opts.preserveTimestamps && process.arch === 'ia32') {
+ const warning = 'Using the preserveTimestamps option in 32-bit ' +
+ 'node is not recommended';
+ process.emitWarning(warning, 'TimestampPrecisionWarning');
+ }
+ const stats = await checkPaths(src, dest, opts);
+ const { srcStat, destStat } = stats;
+ await checkParentPaths(src, srcStat, dest);
+ if (opts.filter) {
+ return handleFilter(checkParentDir, destStat, src, dest, opts);
+ }
+ return checkParentDir(destStat, src, dest, opts);
+}
+
+async function checkPaths(src, dest, opts) {
+ const { 0: srcStat, 1: destStat } = await getStats(src, dest, opts);
+ if (destStat) {
+ if (areIdentical(srcStat, destStat)) {
+ throw new ERR_FS_CP_EINVAL({
+ message: 'src and dest cannot be the same',
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ if (srcStat.isDirectory() && !destStat.isDirectory()) {
+ throw new ERR_FS_CP_DIR_TO_NON_DIR({
+ message: `cannot overwrite directory ${src} ` +
+ `with non-directory ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EISDIR,
+ });
+ }
+ if (!srcStat.isDirectory() && destStat.isDirectory()) {
+ throw new ERR_FS_CP_NON_DIR_TO_DIR({
+ message: `cannot overwrite non-directory ${src} ` +
+ `with directory ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: ENOTDIR,
+ });
+ }
+ }
+
+ if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
+ throw new ERR_FS_CP_EINVAL({
+ message: `cannot copy ${src} to a subdirectory of self ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ return { srcStat, destStat };
+}
+
+function areIdentical(srcStat, destStat) {
+ return destStat.ino && destStat.dev && destStat.ino === srcStat.ino &&
+ destStat.dev === srcStat.dev;
+}
+
+function getStats(src, dest, opts) {
+ const statFunc = opts.dereference ?
+ (file) => stat(file, { bigint: true }) :
+ (file) => lstat(file, { bigint: true });
+ return PromiseAll(new SafeArrayIterator([
+ statFunc(src),
+ PromisePrototypeCatch(statFunc(dest), (err) => {
+ if (err.code === 'ENOENT') return null;
+ throw err;
+ }),
+ ]));
+}
+
+async function checkParentDir(destStat, src, dest, opts) {
+ const destParent = dirname(dest);
+ const dirExists = await pathExists(destParent);
+ if (dirExists) return getStatsForCopy(destStat, src, dest, opts);
+ await mkdir(destParent, { recursive: true });
+ return getStatsForCopy(destStat, src, dest, opts);
+}
+
+function pathExists(dest) {
+ return PromisePrototypeThen(
+ stat(dest),
+ () => true,
+ (err) => (err.code === 'ENOENT' ? false : PromiseReject(err)));
+}
+
+// Recursively check if dest parent is a subdirectory of src.
+// It works for all file types including symlinks since it
+// checks the src and dest inodes. It starts from the deepest
+// parent and stops once it reaches the src parent or the root path.
+async function checkParentPaths(src, srcStat, dest) {
+ const srcParent = resolve(dirname(src));
+ const destParent = resolve(dirname(dest));
+ if (destParent === srcParent || destParent === parse(destParent).root) {
+ return;
+ }
+ let destStat;
+ try {
+ destStat = await stat(destParent, { bigint: true });
+ } catch (err) {
+ if (err.code === 'ENOENT') return;
+ throw err;
+ }
+ if (areIdentical(srcStat, destStat)) {
+ throw new ERR_FS_CP_EINVAL({
+ message: `cannot copy ${src} to a subdirectory of self ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ return checkParentPaths(src, srcStat, destParent);
+}
+
+const normalizePathToArray = (path) =>
+ ArrayPrototypeFilter(StringPrototypeSplit(resolve(path), sep), Boolean);
+
+// Return true if dest is a subdir of src, otherwise false.
+// It only checks the path strings.
+function isSrcSubdir(src, dest) {
+ const srcArr = normalizePathToArray(src);
+ const destArr = normalizePathToArray(dest);
+ return ArrayPrototypeEvery(srcArr, (cur, i) => destArr[i] === cur);
+}
+
+async function handleFilter(onInclude, destStat, src, dest, opts, cb) {
+ const include = await opts.filter(src, dest);
+ if (include) return onInclude(destStat, src, dest, opts, cb);
+}
+
+function startCopy(destStat, src, dest, opts) {
+ if (opts.filter) {
+ return handleFilter(getStatsForCopy, destStat, src, dest, opts);
+ }
+ return getStatsForCopy(destStat, src, dest, opts);
+}
+
+async function getStatsForCopy(destStat, src, dest, opts) {
+ const statFn = opts.dereference ? stat : lstat;
+ const srcStat = await statFn(src);
+ if (srcStat.isDirectory() && opts.recursive) {
+ return onDir(srcStat, destStat, src, dest, opts);
+ } else if (srcStat.isDirectory()) {
+ throw new ERR_FS_EISDIR({
+ message: `${src} is a directory (not copied)`,
+ path: src,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ } else if (srcStat.isFile() ||
+ srcStat.isCharacterDevice() ||
+ srcStat.isBlockDevice()) {
+ return onFile(srcStat, destStat, src, dest, opts);
+ } else if (srcStat.isSymbolicLink()) {
+ return onLink(destStat, src, dest);
+ } else if (srcStat.isSocket()) {
+ throw new ERR_FS_CP_SOCKET({
+ message: `cannot copy a socket file: ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ } else if (srcStat.isFIFO()) {
+ throw new ERR_FS_CP_FIFO_PIPE({
+ message: `cannot copy a FIFO pipe: ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ throw new ERR_FS_CP_UNKNOWN({
+ message: `cannot copy an unknown file type: ${dest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+}
+
+function onFile(srcStat, destStat, src, dest, opts) {
+ if (!destStat) return _copyFile(srcStat, src, dest, opts);
+ return mayCopyFile(srcStat, src, dest, opts);
+}
+
+async function mayCopyFile(srcStat, src, dest, opts) {
+ if (opts.force) {
+ await unlink(dest);
+ return _copyFile(srcStat, src, dest, opts);
+ } else if (opts.errorOnExist) {
+ throw new ERR_FS_CP_EEXIST({
+ message: `${dest} already exists`,
+ path: dest,
+ syscall: 'cp',
+ errno: EEXIST,
+ });
+ }
+}
+
+async function _copyFile(srcStat, src, dest, opts) {
+ await copyFile(src, dest);
+ if (opts.preserveTimestamps) {
+ return handleTimestampsAndMode(srcStat.mode, src, dest);
+ }
+ return setDestMode(dest, srcStat.mode);
+}
+
+async function handleTimestampsAndMode(srcMode, src, dest) {
+ // Make sure the file is writable before setting the timestamp
+ // otherwise open fails with EPERM when invoked with 'r+'
+ // (through utimes call)
+ if (fileIsNotWritable(srcMode)) {
+ await makeFileWritable(dest, srcMode);
+ return setDestTimestampsAndMode(srcMode, src, dest);
+ }
+ return setDestTimestampsAndMode(srcMode, src, dest);
+}
+
+function fileIsNotWritable(srcMode) {
+ return (srcMode & 0o200) === 0;
+}
+
+function makeFileWritable(dest, srcMode) {
+ return setDestMode(dest, srcMode | 0o200);
+}
+
+async function setDestTimestampsAndMode(srcMode, src, dest) {
+ await setDestTimestamps(src, dest);
+ return setDestMode(dest, srcMode);
+}
+
+function setDestMode(dest, srcMode) {
+ return chmod(dest, srcMode);
+}
+
+async function setDestTimestamps(src, dest) {
+ // The initial srcStat.atime cannot be trusted
+ // because it is modified by the read(2) system call
+ // (See https://nodejs.org/api/fs.html#fs_stat_time_values)
+ const updatedSrcStat = await stat(src);
+ return utimes(dest, updatedSrcStat.atime, updatedSrcStat.mtime);
+}
+
+function onDir(srcStat, destStat, src, dest, opts) {
+ if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts);
+ return copyDir(src, dest, opts);
+}
+
+async function mkDirAndCopy(srcMode, src, dest, opts) {
+ await mkdir(dest);
+ await copyDir(src, dest, opts);
+ return setDestMode(dest, srcMode);
+}
+
+async function copyDir(src, dest, opts) {
+ const dir = await readdir(src);
+ for (let i = 0; i < dir.length; i++) {
+ const item = dir[i];
+ const srcItem = join(src, item);
+ const destItem = join(dest, item);
+ const { destStat } = await checkPaths(srcItem, destItem, opts);
+ await startCopy(destStat, srcItem, destItem, opts);
+ }
+}
+
+async function onLink(destStat, src, dest) {
+ let resolvedSrc = await readlink(src);
+ if (!isAbsolute(resolvedSrc)) {
+ resolvedSrc = resolve(dirname(src), resolvedSrc);
+ }
+ if (!destStat) {
+ return symlink(resolvedSrc, dest);
+ }
+ let resolvedDest;
+ try {
+ resolvedDest = await readlink(dest);
+ } catch (err) {
+ // Dest exists and is a regular file or directory,
+ // Windows may throw UNKNOWN error. If dest already exists,
+ // fs throws error anyway, so no need to guard against it here.
+ if (err.code === 'EINVAL' || err.code === 'UNKNOWN') {
+ return symlink(resolvedSrc, dest);
+ }
+ throw err;
+ }
+ if (!isAbsolute(resolvedDest)) {
+ resolvedDest = resolve(dirname(dest), resolvedDest);
+ }
+ if (isSrcSubdir(resolvedSrc, resolvedDest)) {
+ throw new ERR_FS_CP_EINVAL({
+ message: `cannot copy ${resolvedSrc} to a subdirectory of self ` +
+ `${resolvedDest}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ // Do not copy if src is a subdir of dest since unlinking
+ // dest in this case would result in removing src contents
+ // and therefore a broken symlink would be created.
+ const srcStat = await stat(src);
+ if (srcStat.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
+ throw new ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY({
+ message: `cannot overwrite ${resolvedDest} with ${resolvedSrc}`,
+ path: dest,
+ syscall: 'cp',
+ errno: EINVAL,
+ });
+ }
+ return copyLink(resolvedSrc, dest);
+}
+
+async function copyLink(resolvedSrc, dest) {
+ await unlink(dest);
+ return symlink(resolvedSrc, dest);
+}
+
+module.exports = {
+ areIdentical,
+ cpFn,
+ isSrcSubdir,
+};
diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js
index 692d6b42fa5..e26f1314d15 100644
--- a/lib/internal/fs/promises.js
+++ b/lib/internal/fs/promises.js
@@ -59,12 +59,13 @@ const {
stringToSymlinkType,
toUnixTimestamp,
validateBufferArray,
+ validateCpOptions,
validateOffsetLengthRead,
validateOffsetLengthWrite,
validateRmOptions,
validateRmdirOptions,
validateStringAfterArrayBufferView,
- warnOnNonPortableTemplate
+ warnOnNonPortableTemplate,
} = require('internal/fs/utils');
const { opendir } = require('internal/fs/dir');
const {
@@ -109,6 +110,11 @@ const {
const getDirectoryEntriesPromise = promisify(getDirents);
const validateRmOptionsPromise = promisify(validateRmOptions);
+let cpPromises;
+function lazyLoadCpPromises() {
+ return cpPromises ??= require('internal/fs/cp/cp').cpFn;
+}
+
class FileHandle extends EventEmitterMixin(JSTransferable) {
/**
* @param {InternalFSBinding.FileHandle | undefined} filehandle
@@ -433,6 +439,13 @@ async function access(path, mode = F_OK) {
kUsePromises);
}
+async function cp(src, dest, options) {
+ options = validateCpOptions(options);
+ src = pathModule.toNamespacedPath(getValidatedPath(src, 'src'));
+ dest = pathModule.toNamespacedPath(getValidatedPath(dest, 'dest'));
+ return lazyLoadCpPromises()(src, dest, options);
+}
+
async function copyFile(src, dest, mode) {
src = getValidatedPath(src, 'src');
dest = getValidatedPath(dest, 'dest');
@@ -799,6 +812,7 @@ module.exports = {
exports: {
access,
copyFile,
+ cp,
open,
opendir: promisify(opendir),
rename,
diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js
index 322b1519ef2..b0aeb8185e9 100644
--- a/lib/internal/fs/utils.js
+++ b/lib/internal/fs/utils.js
@@ -47,6 +47,7 @@ const { toPathIfFileURL } = require('internal/url');
const {
validateAbortSignal,
validateBoolean,
+ validateFunction,
validateInt32,
validateInteger,
validateObject,
@@ -716,6 +717,15 @@ function warnOnNonPortableTemplate(template) {
}
}
+const defaultCpOptions = {
+ dereference: false,
+ errorOnExist: false,
+ filter: undefined,
+ force: true,
+ preserveTimestamps: false,
+ recursive: false,
+};
+
const defaultRmOptions = {
recursive: false,
force: false,
@@ -729,6 +739,22 @@ const defaultRmdirOptions = {
recursive: false,
};
+const validateCpOptions = hideStackFrames((options) => {
+ if (options === undefined)
+ return { ...defaultCpOptions };
+ validateObject(options, 'options');
+ options = { ...defaultCpOptions, ...options };
+ validateBoolean(options.dereference, 'options.dereference');
+ validateBoolean(options.errorOnExist, 'options.errorOnExist');
+ validateBoolean(options.force, 'options.force');
+ validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps');
+ validateBoolean(options.recursive, 'options.recursive');
+ if (options.filter !== undefined) {
+ validateFunction(options.filter, 'options.filter');
+ }
+ return options;
+});
+
const validateRmOptions = hideStackFrames((path, options, expectDir, cb) => {
options = validateRmdirOptions(options, defaultRmOptions);
validateBoolean(options.force, 'options.force');
@@ -902,6 +928,7 @@ module.exports = {
Stats,
toUnixTimestamp,
validateBufferArray,
+ validateCpOptions,
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
diff --git a/test/fixtures/copy/kitchen-sink/README.md b/test/fixtures/copy/kitchen-sink/README.md
new file mode 100644
index 00000000000..fec56017dc1
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/README.md
@@ -0,0 +1 @@
+# Hello
diff --git a/test/fixtures/copy/kitchen-sink/a/b/README2.md b/test/fixtures/copy/kitchen-sink/a/b/README2.md
new file mode 100644
index 00000000000..fec56017dc1
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/b/README2.md
@@ -0,0 +1 @@
+# Hello
diff --git a/test/fixtures/copy/kitchen-sink/a/b/index.js b/test/fixtures/copy/kitchen-sink/a/b/index.js
new file mode 100644
index 00000000000..12388b0457b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/b/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+ purpose: 'testing copy'
+};
diff --git a/test/fixtures/copy/kitchen-sink/a/c/README2.md b/test/fixtures/copy/kitchen-sink/a/c/README2.md
new file mode 100644
index 00000000000..fec56017dc1
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/c/README2.md
@@ -0,0 +1 @@
+# Hello
diff --git a/test/fixtures/copy/kitchen-sink/a/c/d/README3.md b/test/fixtures/copy/kitchen-sink/a/c/d/README3.md
new file mode 100644
index 00000000000..fec56017dc1
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/c/d/README3.md
@@ -0,0 +1 @@
+# Hello
diff --git a/test/fixtures/copy/kitchen-sink/a/c/d/index.js b/test/fixtures/copy/kitchen-sink/a/c/d/index.js
new file mode 100644
index 00000000000..12388b0457b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/c/d/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+ purpose: 'testing copy'
+};
diff --git a/test/fixtures/copy/kitchen-sink/a/c/index.js b/test/fixtures/copy/kitchen-sink/a/c/index.js
new file mode 100644
index 00000000000..12388b0457b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/c/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+ purpose: 'testing copy'
+};
diff --git a/test/fixtures/copy/kitchen-sink/a/index.js b/test/fixtures/copy/kitchen-sink/a/index.js
new file mode 100644
index 00000000000..12388b0457b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/a/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+ purpose: 'testing copy'
+};
diff --git a/test/fixtures/copy/kitchen-sink/index.js b/test/fixtures/copy/kitchen-sink/index.js
new file mode 100644
index 00000000000..12388b0457b
--- /dev/null
+++ b/test/fixtures/copy/kitchen-sink/index.js
@@ -0,0 +1,3 @@
+module.exports = {
+ purpose: 'testing copy'
+};
diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs
new file mode 100644
index 00000000000..804b5a1f4c3
--- /dev/null
+++ b/test/parallel/test-fs-cp.mjs
@@ -0,0 +1,763 @@
+import { mustCall } from '../common/index.mjs';
+
+import assert from 'assert';
+import fs from 'fs';
+const {
+ cp,
+ cpSync,
+ lstatSync,
+ mkdirSync,
+ readdirSync,
+ readFileSync,
+ readlinkSync,
+ symlinkSync,
+ statSync,
+ writeFileSync,
+} = fs;
+import net from 'net';
+import { join } from 'path';
+import { pathToFileURL } from 'url';
+import { setTimeout } from 'timers/promises';
+
+const isWindows = process.platform === 'win32';
+import tmpdir from '../common/tmpdir.js';
+tmpdir.refresh();
+
+let dirc = 0;
+function nextdir() {
+ return join(tmpdir.path, `copy_${++dirc}`);
+}
+
+// Synchronous implementation of copy.
+
+// It copies a nested folder structure with files and folders.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cpSync(src, dest, { recursive: true });
+ assertDirEquivalent(src, dest);
+}
+
+// It does not throw errors when directory is copied over and force is false.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a', 'b'), { recursive: true });
+ writeFileSync(join(src, 'README.md'), 'hello world', 'utf8');
+ const dest = nextdir();
+ cpSync(src, dest, { recursive: true });
+ const initialStat = lstatSync(join(dest, 'README.md'));
+ cpSync(src, dest, { force: false, recursive: true });
+ // File should not have been copied over, so access times will be identical:
+ assertDirEquivalent(src, dest);
+ const finalStat = lstatSync(join(dest, 'README.md'));
+ assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime());
+}
+
+// It overwrites existing files if force is true.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ mkdirSync(dest, { recursive: true });
+ writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8');
+ cpSync(src, dest, { recursive: true });
+ assertDirEquivalent(src, dest);
+ const content = readFileSync(join(dest, 'README.md'), 'utf8');
+ assert.strictEqual(content.trim(), '# Hello');
+}
+
+// It does not fail if the same directory is copied to dest twice,
+// when dereference is true, and force is false (fails silently).
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ const destFile = join(dest, 'a/b/README2.md');
+ cpSync(src, dest, { dereference: true, recursive: true });
+ cpSync(src, dest, { dereference: true, recursive: true });
+ const stat = lstatSync(destFile);
+ assert(stat.isFile());
+}
+
+
+// It copies file itself, rather than symlink, when dereference is true.
+{
+ const src = nextdir();
+ mkdirSync(src, { recursive: true });
+ writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
+ symlinkSync(join(src, 'foo.js'), join(src, 'bar.js'));
+
+ const dest = nextdir();
+ mkdirSync(dest, { recursive: true });
+ const destFile = join(dest, 'foo.js');
+
+ cpSync(join(src, 'bar.js'), destFile, { dereference: true, recursive: true });
+ const stat = lstatSync(destFile);
+ assert(stat.isFile());
+}
+
+
+// It throws error when src and dest are identical.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ assert.throws(
+ () => cpSync(src, src),
+ { code: 'ERR_FS_CP_EINVAL' }
+ );
+}
+
+// It throws error if symlink in src points to location in dest.
+{
+ const src = nextdir();
+ mkdirSync(src, { recursive: true });
+ const dest = nextdir();
+ mkdirSync(dest);
+ symlinkSync(dest, join(src, 'link'));
+ cpSync(src, dest, { recursive: true });
+ assert.throws(
+ () => cpSync(src, dest, { recursive: true }),
+ {
+ code: 'ERR_FS_CP_EINVAL'
+ }
+ );
+}
+
+// It throws error if symlink in dest points to location in src.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a', 'b'), { recursive: true });
+ symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c'));
+
+ const dest = nextdir();
+ mkdirSync(join(dest, 'a'), { recursive: true });
+ symlinkSync(src, join(dest, 'a', 'c'));
+ assert.throws(
+ () => cpSync(src, dest, { recursive: true }),
+ { code: 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY' }
+ );
+}
+
+// It throws error if parent directory of symlink in dest points to src.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a'), { recursive: true });
+ const dest = nextdir();
+ // Create symlink in dest pointing to src.
+ const destLink = join(dest, 'b');
+ mkdirSync(dest, { recursive: true });
+ symlinkSync(src, destLink);
+ assert.throws(
+ () => cpSync(src, join(dest, 'b', 'c')),
+ { code: 'ERR_FS_CP_EINVAL' }
+ );
+}
+
+// It throws error if attempt is made to copy directory to file.
+{
+ const src = nextdir();
+ mkdirSync(src, { recursive: true });
+ const dest = './test/fixtures/copy/kitchen-sink/README.md';
+ assert.throws(
+ () => cpSync(src, dest),
+ { code: 'ERR_FS_CP_DIR_TO_NON_DIR' }
+ );
+}
+
+// It allows file to be copied to a file path.
+{
+ const srcFile = './test/fixtures/copy/kitchen-sink/index.js';
+ const destFile = join(nextdir(), 'index.js');
+ cpSync(srcFile, destFile, { dereference: true });
+ const stat = lstatSync(destFile);
+ assert(stat.isFile());
+}
+
+// It throws error if directory copied without recursive flag.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ assert.throws(
+ () => cpSync(src, dest),
+ { code: 'ERR_FS_EISDIR' }
+ );
+}
+
+
+// It throws error if attempt is made to copy file to directory.
+{
+ const src = './test/fixtures/copy/kitchen-sink/README.md';
+ const dest = nextdir();
+ mkdirSync(dest, { recursive: true });
+ assert.throws(
+ () => cpSync(src, dest),
+ { code: 'ERR_FS_CP_NON_DIR_TO_DIR' }
+ );
+}
+
+// It throws error if attempt is made to copy to subdirectory of self.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = './test/fixtures/copy/kitchen-sink/a';
+ assert.throws(
+ () => cpSync(src, dest),
+ { code: 'ERR_FS_CP_EINVAL' }
+ );
+}
+
+// It throws an error if attempt is made to copy socket.
+if (!isWindows) {
+ const dest = nextdir();
+ const sock = `${process.pid}.sock`;
+ const server = net.createServer();
+ server.listen(sock);
+ assert.throws(
+ () => cpSync(sock, dest),
+ { code: 'ERR_FS_CP_SOCKET' }
+ );
+ server.close();
+}
+
+// It copies timestamps from src to dest if preserveTimestamps is true.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cpSync(src, dest, { preserveTimestamps: true, recursive: true });
+ assertDirEquivalent(src, dest);
+ const srcStat = lstatSync(join(src, 'index.js'));
+ const destStat = lstatSync(join(dest, 'index.js'));
+ assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
+}
+
+// It applies filter function.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cpSync(src, dest, {
+ filter: (path) => {
+ const pathStat = statSync(path);
+ return pathStat.isDirectory() || path.endsWith('.js');
+ },
+ dereference: true,
+ recursive: true,
+ });
+ const destEntries = [];
+ collectEntries(dest, destEntries);
+ for (const entry of destEntries) {
+ assert.strictEqual(
+ entry.isDirectory() || entry.name.endsWith('.js'),
+ true
+ );
+ }
+}
+
+// It throws error if filter function is asynchronous.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ assert.throws(() => {
+ cpSync(src, dest, {
+ filter: async (path) => {
+ await setTimeout(5, 'done');
+ const pathStat = statSync(path);
+ return pathStat.isDirectory() || path.endsWith('.js');
+ },
+ dereference: true,
+ recursive: true,
+ });
+ }, { code: 'ERR_INVALID_RETURN_VALUE' });
+}
+
+// It throws error if errorOnExist is true, force is false, and file or folder
+// copied over.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cpSync(src, dest, { recursive: true });
+ assert.throws(
+ () => cpSync(src, dest, {
+ dereference: true,
+ errorOnExist: true,
+ force: false,
+ recursive: true,
+ }),
+ { code: 'ERR_FS_CP_EEXIST' }
+ );
+}
+
+// It throws EEXIST error if attempt is made to copy symlink over file.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a', 'b'), { recursive: true });
+ symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c'));
+
+ const dest = nextdir();
+ mkdirSync(join(dest, 'a'), { recursive: true });
+ writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8');
+ assert.throws(
+ () => cpSync(src, dest, { recursive: true }),
+ { code: 'EEXIST' }
+ );
+}
+
+// It makes file writeable when updating timestamp, if not writeable.
+{
+ const src = nextdir();
+ mkdirSync(src, { recursive: true });
+ const dest = nextdir();
+ mkdirSync(dest, { recursive: true });
+ writeFileSync(join(src, 'foo.txt'), 'foo', { mode: 0o444 });
+ cpSync(src, dest, { preserveTimestamps: true, recursive: true });
+ assertDirEquivalent(src, dest);
+ const srcStat = lstatSync(join(src, 'foo.txt'));
+ const destStat = lstatSync(join(dest, 'foo.txt'));
+ assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
+}
+
+// It copies link if it does not point to folder in src.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a', 'b'), { recursive: true });
+ symlinkSync(src, join(src, 'a', 'c'));
+ const dest = nextdir();
+ mkdirSync(join(dest, 'a'), { recursive: true });
+ symlinkSync(dest, join(dest, 'a', 'c'));
+ cpSync(src, dest, { recursive: true });
+ const link = readlinkSync(join(dest, 'a', 'c'));
+ assert.strictEqual(link, src);
+}
+
+// It accepts file URL as src and dest.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cpSync(pathToFileURL(src), pathToFileURL(dest), { recursive: true });
+ assertDirEquivalent(src, dest);
+}
+
+// It throws if options is not object.
+{
+ assert.throws(
+ () => cpSync('a', 'b', () => {}),
+ { code: 'ERR_INVALID_ARG_TYPE' }
+ );
+}
+
+// Callback implementation of copy.
+
+// It copies a nested folder structure with files and folders.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cp(src, dest, { recursive: true }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ assertDirEquivalent(src, dest);
+ }));
+}
+
+// It does not throw errors when directory is copied over and force is false.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a', 'b'), { recursive: true });
+ writeFileSync(join(src, 'README.md'), 'hello world', 'utf8');
+ const dest = nextdir();
+ cpSync(src, dest, { dereference: true, recursive: true });
+ const initialStat = lstatSync(join(dest, 'README.md'));
+ cp(src, dest, {
+ dereference: true,
+ force: false,
+ recursive: true,
+ }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ assertDirEquivalent(src, dest);
+ // File should not have been copied over, so access times will be identical:
+ const finalStat = lstatSync(join(dest, 'README.md'));
+ assert.strictEqual(finalStat.ctime.getTime(), initialStat.ctime.getTime());
+ }));
+}
+
+// It overwrites existing files if force is true.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ mkdirSync(dest, { recursive: true });
+ writeFileSync(join(dest, 'README.md'), '# Goodbye', 'utf8');
+
+ cp(src, dest, { recursive: true }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ assertDirEquivalent(src, dest);
+ const content = readFileSync(join(dest, 'README.md'), 'utf8');
+ assert.strictEqual(content.trim(), '# Hello');
+ }));
+}
+
+// It does not fail if the same directory is copied to dest twice,
+// when dereference is true, and force is false (fails silently).
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ const destFile = join(dest, 'a/b/README2.md');
+ cpSync(src, dest, { dereference: true, recursive: true });
+ cp(src, dest, {
+ dereference: true,
+ recursive: true
+ }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ const stat = lstatSync(destFile);
+ assert(stat.isFile());
+ }));
+}
+
+// It copies file itself, rather than symlink, when dereference is true.
+{
+ const src = nextdir();
+ mkdirSync(src, { recursive: true });
+ writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
+ symlinkSync(join(src, 'foo.js'), join(src, 'bar.js'));
+
+ const dest = nextdir();
+ mkdirSync(dest, { recursive: true });
+ const destFile = join(dest, 'foo.js');
+
+ cp(join(src, 'bar.js'), destFile, { dereference: true },
+ mustCall((err) => {
+ assert.strictEqual(err, null);
+ const stat = lstatSync(destFile);
+ assert(stat.isFile());
+ })
+ );
+}
+
+// It returns error when src and dest are identical.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ cp(src, src, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL');
+ }));
+}
+
+// It returns error if symlink in src points to location in dest.
+{
+ const src = nextdir();
+ mkdirSync(src, { recursive: true });
+ const dest = nextdir();
+ mkdirSync(dest);
+ symlinkSync(dest, join(src, 'link'));
+ cpSync(src, dest, { recursive: true });
+ cp(src, dest, { recursive: true }, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL');
+ }));
+}
+
+// It returns error if symlink in dest points to location in src.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a', 'b'), { recursive: true });
+ symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c'));
+
+ const dest = nextdir();
+ mkdirSync(join(dest, 'a'), { recursive: true });
+ symlinkSync(src, join(dest, 'a', 'c'));
+ cp(src, dest, { recursive: true }, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY');
+ }));
+}
+
+// It returns error if parent directory of symlink in dest points to src.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a'), { recursive: true });
+ const dest = nextdir();
+ // Create symlink in dest pointing to src.
+ const destLink = join(dest, 'b');
+ mkdirSync(dest, { recursive: true });
+ symlinkSync(src, destLink);
+ cp(src, join(dest, 'b', 'c'), mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL');
+ }));
+}
+
+// It returns error if attempt is made to copy directory to file.
+{
+ const src = nextdir();
+ mkdirSync(src, { recursive: true });
+ const dest = './test/fixtures/copy/kitchen-sink/README.md';
+ cp(src, dest, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_DIR_TO_NON_DIR');
+ }));
+}
+
+// It allows file to be copied to a file path.
+{
+ const srcFile = './test/fixtures/copy/kitchen-sink/README.md';
+ const destFile = join(nextdir(), 'index.js');
+ cp(srcFile, destFile, { dereference: true }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ const stat = lstatSync(destFile);
+ assert(stat.isFile());
+ }));
+}
+
+// It returns error if directory copied without recursive flag.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cp(src, dest, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_EISDIR');
+ }));
+}
+
+// It returns error if attempt is made to copy file to directory.
+{
+ const src = './test/fixtures/copy/kitchen-sink/README.md';
+ const dest = nextdir();
+ mkdirSync(dest, { recursive: true });
+ cp(src, dest, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_NON_DIR_TO_DIR');
+ }));
+}
+
+// It returns error if attempt is made to copy to subdirectory of self.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = './test/fixtures/copy/kitchen-sink/a';
+ cp(src, dest, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_EINVAL');
+ }));
+}
+
+// It returns an error if attempt is made to copy socket.
+if (!isWindows) {
+ const dest = nextdir();
+ const sock = `${process.pid}.sock`;
+ const server = net.createServer();
+ server.listen(sock);
+ cp(sock, dest, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_SOCKET');
+ server.close();
+ }));
+}
+
+// It copies timestamps from src to dest if preserveTimestamps is true.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cp(src, dest, {
+ preserveTimestamps: true,
+ recursive: true
+ }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ assertDirEquivalent(src, dest);
+ const srcStat = lstatSync(join(src, 'index.js'));
+ const destStat = lstatSync(join(dest, 'index.js'));
+ assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
+ }));
+}
+
+// It applies filter function.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cp(src, dest, {
+ filter: (path) => {
+ const pathStat = statSync(path);
+ return pathStat.isDirectory() || path.endsWith('.js');
+ },
+ dereference: true,
+ recursive: true,
+ }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ const destEntries = [];
+ collectEntries(dest, destEntries);
+ for (const entry of destEntries) {
+ assert.strictEqual(
+ entry.isDirectory() || entry.name.endsWith('.js'),
+ true
+ );
+ }
+ }));
+}
+
+// It supports async filter function.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cp(src, dest, {
+ filter: async (path) => {
+ await setTimeout(5, 'done');
+ const pathStat = statSync(path);
+ return pathStat.isDirectory() || path.endsWith('.js');
+ },
+ dereference: true,
+ recursive: true,
+ }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ const destEntries = [];
+ collectEntries(dest, destEntries);
+ for (const entry of destEntries) {
+ assert.strictEqual(
+ entry.isDirectory() || entry.name.endsWith('.js'),
+ true
+ );
+ }
+ }));
+}
+
+// It returns error if errorOnExist is true, force is false, and file or folder
+// copied over.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cpSync(src, dest, { recursive: true });
+ cp(src, dest, {
+ dereference: true,
+ errorOnExist: true,
+ force: false,
+ recursive: true,
+ }, mustCall((err) => {
+ assert.strictEqual(err.code, 'ERR_FS_CP_EEXIST');
+ }));
+}
+
+// It returns EEXIST error if attempt is made to copy symlink over file.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a', 'b'), { recursive: true });
+ symlinkSync(join(src, 'a', 'b'), join(src, 'a', 'c'));
+
+ const dest = nextdir();
+ mkdirSync(join(dest, 'a'), { recursive: true });
+ writeFileSync(join(dest, 'a', 'c'), 'hello', 'utf8');
+ cp(src, dest, { recursive: true }, mustCall((err) => {
+ assert.strictEqual(err.code, 'EEXIST');
+ }));
+}
+
+// It makes file writeable when updating timestamp, if not writeable.
+{
+ const src = nextdir();
+ mkdirSync(src, { recursive: true });
+ const dest = nextdir();
+ mkdirSync(dest, { recursive: true });
+ writeFileSync(join(src, 'foo.txt'), 'foo', { mode: 0o444 });
+ cp(src, dest, {
+ preserveTimestamps: true,
+ recursive: true,
+ }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ assertDirEquivalent(src, dest);
+ const srcStat = lstatSync(join(src, 'foo.txt'));
+ const destStat = lstatSync(join(dest, 'foo.txt'));
+ assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
+ }));
+}
+
+// It copies link if it does not point to folder in src.
+{
+ const src = nextdir();
+ mkdirSync(join(src, 'a', 'b'), { recursive: true });
+ symlinkSync(src, join(src, 'a', 'c'));
+ const dest = nextdir();
+ mkdirSync(join(dest, 'a'), { recursive: true });
+ symlinkSync(dest, join(dest, 'a', 'c'));
+ cp(src, dest, { recursive: true }, mustCall((err) => {
+ assert.strictEqual(err, null);
+ const link = readlinkSync(join(dest, 'a', 'c'));
+ assert.strictEqual(link, src);
+ }));
+}
+
+// It accepts file URL as src and dest.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ cp(pathToFileURL(src), pathToFileURL(dest), { recursive: true },
+ mustCall((err) => {
+ assert.strictEqual(err, null);
+ assertDirEquivalent(src, dest);
+ }));
+}
+
+// It throws if options is not object.
+{
+ assert.throws(
+ () => cp('a', 'b', 'hello', () => {}),
+ { code: 'ERR_INVALID_ARG_TYPE' }
+ );
+}
+
+// Promises implementation of copy.
+
+// It copies a nested folder structure with files and folders.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ const p = await fs.promises.cp(src, dest, { recursive: true });
+ assert.strictEqual(p, undefined);
+ assertDirEquivalent(src, dest);
+}
+
+// It accepts file URL as src and dest.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ const p = await fs.promises.cp(
+ pathToFileURL(src),
+ pathToFileURL(dest),
+ { recursive: true }
+ );
+ assert.strictEqual(p, undefined);
+ assertDirEquivalent(src, dest);
+}
+
+// It allows async error to be caught.
+{
+ const src = './test/fixtures/copy/kitchen-sink';
+ const dest = nextdir();
+ await fs.promises.cp(src, dest, { recursive: true });
+ await assert.rejects(
+ fs.promises.cp(src, dest, {
+ dereference: true,
+ errorOnExist: true,
+ force: false,
+ recursive: true,
+ }),
+ { code: 'ERR_FS_CP_EEXIST' }
+ );
+}
+
+// It rejects if options is not object.
+{
+ await assert.rejects(
+ fs.promises.cp('a', 'b', () => {}),
+ { code: 'ERR_INVALID_ARG_TYPE' }
+ );
+}
+
+function assertDirEquivalent(dir1, dir2) {
+ const dir1Entries = [];
+ collectEntries(dir1, dir1Entries);
+ const dir2Entries = [];
+ collectEntries(dir2, dir2Entries);
+ assert.strictEqual(dir1Entries.length, dir2Entries.length);
+ for (const entry1 of dir1Entries) {
+ const entry2 = dir2Entries.find((entry) => {
+ return entry.name === entry1.name;
+ });
+ assert(entry2, `entry ${entry2.name} not copied`);
+ if (entry1.isFile()) {
+ assert(entry2.isFile(), `${entry2.name} was not file`);
+ } else if (entry1.isDirectory()) {
+ assert(entry2.isDirectory(), `${entry2.name} was not directory`);
+ } else if (entry1.isSymbolicLink()) {
+ assert(entry2.isSymbolicLink(), `${entry2.name} was not symlink`);
+ }
+ }
+}
+
+function collectEntries(dir, dirEntries) {
+ const newEntries = readdirSync(dir, { withFileTypes: true });
+ for (const entry of newEntries) {
+ if (entry.isDirectory()) {
+ collectEntries(join(dir, entry.name), dirEntries);
+ }
+ }
+ dirEntries.push(...newEntries);
+}
diff --git a/tools/license-builder.sh b/tools/license-builder.sh
index 3d8e615a0e6..6a3a6984aef 100755
--- a/tools/license-builder.sh
+++ b/tools/license-builder.sh
@@ -112,4 +112,7 @@ addlicense "uvwasi" "deps/uvwasi" "$(cat "${rootdir}"/deps/uvwasi/LICENSE)"
addlicense "ngtcp2" "deps/ngtcp2/ngtcp2/" "$(cat "${rootdir}"/deps/ngtcp2/LICENSE_ngtcp2)"
addlicense "nghttp3" "deps/ngtcp2/nghttp3/" "$(cat "${rootdir}"/deps/ngtcp2/LICENSE_nghttp3)"
+addlicense "node-fs-extra" "lib/internal/fs/cp" \
+ "$(curl -sL https://raw.githubusercontent.com/jprichardson/node-fs-extra/b34da2762a4865b025cac06d02d6a2f1f1027b65/LICENSE)"
+
mv "$tmplicense" "$licensefile"