summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorbcoe <bencoe@google.com>2021-08-08 13:48:01 -0700
committerBenjamin Coe <bencoe@google.com>2021-08-11 19:53:32 -0700
commit87d6fd7e696ee02178a8dc33a51e8e59bdc61d68 (patch)
treee9c76b5f1d9f4ba4f51528b2ef8e41b82b6e2f4f /lib
parent4ece669c6205ec78abfdadfe78869bbb8411463e (diff)
downloadnode-new-87d6fd7e696ee02178a8dc33a51e8e59bdc61d68.tar.gz
fs: add recursive cp method
Introduces recursive cp method, based on fs-extra implementation. PR-URL: https://github.com/nodejs/node/pull/39372 Fixes: https://github.com/nodejs/node/issues/35880 Refs: https://github.com/nodejs/tooling/issues/98 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Michaƫl Zasso <targos@protonmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ian Sutherland <ian@iansutherland.ca>
Diffstat (limited to 'lib')
-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
6 files changed, 818 insertions, 1 deletions
diff --git a/lib/fs.js b/lib/fs.js
index cf3c885b31..7e126b84ad 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 25b847b140..7541d7d1eb 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 0000000000..ca2102aeb5
--- /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 0000000000..9ee661be68
--- /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 692d6b42fa..e26f1314d1 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 322b1519ef..b0aeb8185e 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,