From eed1481e33617eef1f2126e80c4cb243a34a365a Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 11:01:56 +0100 Subject: [PATCH 01/12] fs: support copy of relative links with cp and cpSync Fixes: https://github.com/nodejs/node/issues/41693 --- doc/api/fs.md | 6 ++++++ lib/internal/fs/cp/cp-sync.js | 6 +++--- lib/internal/fs/cp/cp.js | 6 +++--- lib/internal/fs/utils.js | 2 ++ test/parallel/test-fs-cp.mjs | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index f0a9c9291e0603..bb496a90c90705 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -896,6 +896,8 @@ added: v16.7.0 * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` + * `absolute` {boolean} convert relative symlinks to absolute symlinks. + **Default:** `true` * Returns: {Promise} Fulfills with `undefined` upon success. Asynchronously copies the entire directory structure from `src` to `dest`, @@ -2119,6 +2121,8 @@ changes: * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` + * `absolute` {boolean} convert relative symlinks to absolute symlinks. + **Default:** `true` * `callback` {Function} Asynchronously copies the entire directory structure from `src` to `dest`, @@ -4875,6 +4879,8 @@ added: v16.7.0 * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` + * `absolute` {boolean} convert relative symlinks to absolute symlinks. + **Default:** `true` Synchronously copies the entire directory structure from `src` to `dest`, including subdirectories and files. diff --git a/lib/internal/fs/cp/cp-sync.js b/lib/internal/fs/cp/cp-sync.js index 497c8c57c317dd..574aea84d686d4 100644 --- a/lib/internal/fs/cp/cp-sync.js +++ b/lib/internal/fs/cp/cp-sync.js @@ -182,7 +182,7 @@ function getStats(destStat, src, dest, opts) { srcStat.isBlockDevice()) { return onFile(srcStat, destStat, src, dest, opts); } else if (srcStat.isSymbolicLink()) { - return onLink(destStat, src, dest); + return onLink(destStat, src, dest, opts); } else if (srcStat.isSocket()) { throw new ERR_FS_CP_SOCKET({ message: `cannot copy a socket file: ${dest}`, @@ -293,9 +293,9 @@ function copyDir(src, dest, opts) { } } -function onLink(destStat, src, dest) { +function onLink(destStat, src, dest, opts) { let resolvedSrc = readlinkSync(src); - if (!isAbsolute(resolvedSrc)) { + if (opts.absolute && !isAbsolute(resolvedSrc)) { resolvedSrc = resolve(dirname(src), resolvedSrc); } if (!destStat) { diff --git a/lib/internal/fs/cp/cp.js b/lib/internal/fs/cp/cp.js index 6dc212f2f6a5fc..4ed061879f1142 100644 --- a/lib/internal/fs/cp/cp.js +++ b/lib/internal/fs/cp/cp.js @@ -222,7 +222,7 @@ async function getStatsForCopy(destStat, src, dest, opts) { srcStat.isBlockDevice()) { return onFile(srcStat, destStat, src, dest, opts); } else if (srcStat.isSymbolicLink()) { - return onLink(destStat, src, dest); + return onLink(destStat, src, dest, opts); } else if (srcStat.isSocket()) { throw new ERR_FS_CP_SOCKET({ message: `cannot copy a socket file: ${dest}`, @@ -335,9 +335,9 @@ async function copyDir(src, dest, opts) { } } -async function onLink(destStat, src, dest) { +async function onLink(destStat, src, dest, opts) { let resolvedSrc = await readlink(src); - if (!isAbsolute(resolvedSrc)) { + if (opts.absolute && !isAbsolute(resolvedSrc)) { resolvedSrc = resolve(dirname(src), resolvedSrc); } if (!destStat) { diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index 66b5e39b0c0fc8..e8d4da7c66e053 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -724,6 +724,7 @@ const defaultCpOptions = { force: true, preserveTimestamps: false, recursive: false, + absolute: true, }; const defaultRmOptions = { @@ -749,6 +750,7 @@ const validateCpOptions = hideStackFrames((options) => { validateBoolean(options.force, 'options.force'); validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps'); validateBoolean(options.recursive, 'options.recursive'); + validateBoolean(options.absolute, 'options.absolute'); if (options.filter !== undefined) { validateFunction(options.filter, 'options.filter'); } diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index 804b5a1f4c322c..1a806383746e37 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -95,6 +95,39 @@ function nextdir() { } +// It resolves relative symlinks to absolute path when absolute is true +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); + symlinkSync('foo.js', join(src, 'bar.js')); + + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + + cpSync(src, dest, { recursive: true, absolute: true }); + const link = readlinkSync(join(dest, 'bar.js')); + assert.strictEqual(link, join(src, 'foo.js')); +} + + +// It copies relative symlink when absolute is false +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); + symlinkSync('foo.js', join(src, 'bar.js')); + + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + const destFile = join(dest, 'foo.js'); + + cpSync(src, dest, { recursive: true, absolute: false }); + const link = readlinkSync(join(dest, 'bar.js')); + assert.strictEqual(link, 'foo.js'); +} + + // It throws error when src and dest are identical. { const src = './test/fixtures/copy/kitchen-sink'; From 65d214c5471a7ff98366301279066f4eb8781adb Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 11:22:30 +0100 Subject: [PATCH 02/12] Fix linter warning Signed-off-by: Marcos Bjoerkelund --- test/parallel/test-fs-cp.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index 1a806383746e37..28470e97fda031 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -120,7 +120,6 @@ function nextdir() { const dest = nextdir(); mkdirSync(dest, { recursive: true }); - const destFile = join(dest, 'foo.js'); cpSync(src, dest, { recursive: true, absolute: false }); const link = readlinkSync(join(dest, 'bar.js')); From f3bd1f2dedb5afde1b8e5d21a375693c3194b4a5 Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 12:35:15 +0100 Subject: [PATCH 03/12] Ensure 'absolute' is a boolean and true by default Signed-off-by: Marcos Bjoerkelund --- test/parallel/test-fs-cp.mjs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index 28470e97fda031..3003064aa8245e 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -95,6 +95,34 @@ function nextdir() { } +// It throws error when absolute is not a boolean +{ + const src = './test/fixtures/copy/kitchen-sink'; + [1, [], {}, null, 1n, undefined, null].forEach((absolute) => { + assert.throws( + () => cpSync(src, src, {absolute}), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + }); +} + + +// It resolves relative symlinks to absolute path by default +{ + const src = nextdir(); + mkdirSync(src, { recursive: true }); + writeFileSync(join(src, 'foo.js'), 'foo', 'utf8'); + symlinkSync('foo.js', join(src, 'bar.js')); + + const dest = nextdir(); + mkdirSync(dest, { recursive: true }); + + cpSync(src, dest, { recursive: true }); + const link = readlinkSync(join(dest, 'bar.js')); + assert.strictEqual(link, join(src, 'foo.js')); +} + + // It resolves relative symlinks to absolute path when absolute is true { const src = nextdir(); From df8024dca2d40710d2b65a89c938f6b1cc3508df Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 13:25:18 +0100 Subject: [PATCH 04/12] Change absolute approach to verbatimSymlinks Signed-off-by: Marcos Bjoerkelund --- doc/api/fs.md | 12 ++++++------ lib/internal/fs/cp/cp-sync.js | 2 +- lib/internal/fs/cp/cp.js | 2 +- lib/internal/fs/utils.js | 4 ++-- test/parallel/test-fs-cp.mjs | 16 ++++++++-------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index bb496a90c90705..7c6f5dd7bcac4f 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -896,8 +896,8 @@ added: v16.7.0 * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` - * `absolute` {boolean} convert relative symlinks to absolute symlinks. - **Default:** `true` + * `verbatimSymlinks` {boolean} When `true` path resolution for symlinks will + be skipped. **Default:** `false` * Returns: {Promise} Fulfills with `undefined` upon success. Asynchronously copies the entire directory structure from `src` to `dest`, @@ -2121,8 +2121,8 @@ changes: * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` - * `absolute` {boolean} convert relative symlinks to absolute symlinks. - **Default:** `true` + * `verbatimSymlinks` {boolean} When `true` path resolution for symlinks will + be skipped. **Default:** `false` * `callback` {Function} Asynchronously copies the entire directory structure from `src` to `dest`, @@ -4879,8 +4879,8 @@ added: v16.7.0 * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` - * `absolute` {boolean} convert relative symlinks to absolute symlinks. - **Default:** `true` + * `verbatimSymlinks` {boolean} When `true` path resolution for symlinks will + be skipped. **Default:** `false` Synchronously copies the entire directory structure from `src` to `dest`, including subdirectories and files. diff --git a/lib/internal/fs/cp/cp-sync.js b/lib/internal/fs/cp/cp-sync.js index 574aea84d686d4..f9d159a193107e 100644 --- a/lib/internal/fs/cp/cp-sync.js +++ b/lib/internal/fs/cp/cp-sync.js @@ -295,7 +295,7 @@ function copyDir(src, dest, opts) { function onLink(destStat, src, dest, opts) { let resolvedSrc = readlinkSync(src); - if (opts.absolute && !isAbsolute(resolvedSrc)) { + if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) { resolvedSrc = resolve(dirname(src), resolvedSrc); } if (!destStat) { diff --git a/lib/internal/fs/cp/cp.js b/lib/internal/fs/cp/cp.js index 4ed061879f1142..bcbc8aa3279a50 100644 --- a/lib/internal/fs/cp/cp.js +++ b/lib/internal/fs/cp/cp.js @@ -337,7 +337,7 @@ async function copyDir(src, dest, opts) { async function onLink(destStat, src, dest, opts) { let resolvedSrc = await readlink(src); - if (opts.absolute && !isAbsolute(resolvedSrc)) { + if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) { resolvedSrc = resolve(dirname(src), resolvedSrc); } if (!destStat) { diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index e8d4da7c66e053..c009c9d7674206 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -724,7 +724,7 @@ const defaultCpOptions = { force: true, preserveTimestamps: false, recursive: false, - absolute: true, + verbatimSymlinks: false, }; const defaultRmOptions = { @@ -750,7 +750,7 @@ const validateCpOptions = hideStackFrames((options) => { validateBoolean(options.force, 'options.force'); validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps'); validateBoolean(options.recursive, 'options.recursive'); - validateBoolean(options.absolute, 'options.absolute'); + validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks'); if (options.filter !== undefined) { validateFunction(options.filter, 'options.filter'); } diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index 3003064aa8245e..f529d294577924 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -95,19 +95,19 @@ function nextdir() { } -// It throws error when absolute is not a boolean +// It throws error when verbatimSymlinks is not a boolean. { const src = './test/fixtures/copy/kitchen-sink'; - [1, [], {}, null, 1n, undefined, null].forEach((absolute) => { + [1, [], {}, null, 1n, undefined, null].forEach((verbatimSymlinks) => { assert.throws( - () => cpSync(src, src, {absolute}), + () => cpSync(src, src, {verbatimSymlinks}), { code: 'ERR_INVALID_ARG_TYPE' } ); }); } -// It resolves relative symlinks to absolute path by default +// It resolves relative symlinks to their absolute path by default. { const src = nextdir(); mkdirSync(src, { recursive: true }); @@ -123,7 +123,7 @@ function nextdir() { } -// It resolves relative symlinks to absolute path when absolute is true +// It resolves relative symlinks when verbatimSymlinks is false. { const src = nextdir(); mkdirSync(src, { recursive: true }); @@ -133,13 +133,13 @@ function nextdir() { const dest = nextdir(); mkdirSync(dest, { recursive: true }); - cpSync(src, dest, { recursive: true, absolute: true }); + cpSync(src, dest, { recursive: true, verbatimSymlinks: false }); const link = readlinkSync(join(dest, 'bar.js')); assert.strictEqual(link, join(src, 'foo.js')); } -// It copies relative symlink when absolute is false +// It does not resolve relative symlinks when verbatimSymlinks is true. { const src = nextdir(); mkdirSync(src, { recursive: true }); @@ -149,7 +149,7 @@ function nextdir() { const dest = nextdir(); mkdirSync(dest, { recursive: true }); - cpSync(src, dest, { recursive: true, absolute: false }); + cpSync(src, dest, { recursive: true, verbatimSymlinks: true }); const link = readlinkSync(join(dest, 'bar.js')); assert.strictEqual(link, 'foo.js'); } From 3071c63d9169e4268d555b85717efd728d35352c Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 13:31:41 +0100 Subject: [PATCH 05/12] Add changes entries in YAML docs for fs.cp/fs.cpSync/fs.promises.cp Signed-off-by: Marcos Bjoerkelund --- doc/api/fs.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/api/fs.md b/doc/api/fs.md index 7c6f5dd7bcac4f..26dadab1368869 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -876,6 +876,11 @@ try { > Stability: 1 - Experimental @@ -2096,6 +2101,10 @@ copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback); > Stability: 1 - Experimental From 4c1324e4ca9fdb3ce4457f44f92cd0548bec3ce6 Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 13:51:13 +0100 Subject: [PATCH 06/12] Fix linter warning Signed-off-by: Marcos Bjoerkelund --- test/parallel/test-fs-cp.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index f529d294577924..965bab8c4abe9c 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -100,7 +100,7 @@ function nextdir() { const src = './test/fixtures/copy/kitchen-sink'; [1, [], {}, null, 1n, undefined, null].forEach((verbatimSymlinks) => { assert.throws( - () => cpSync(src, src, {verbatimSymlinks}), + () => cpSync(src, src, { verbatimSymlinks }), { code: 'ERR_INVALID_ARG_TYPE' } ); }); From 26c333a6d7db74b09dff4d28c94d98a01c2d7ad6 Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 17:48:25 +0100 Subject: [PATCH 07/12] Apply wording suggestions Signed-off-by: Marcos Bjoerkelund --- doc/api/fs.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index 26dadab1368869..33bfafaa438bea 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -901,7 +901,7 @@ changes: * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` - * `verbatimSymlinks` {boolean} When `true` path resolution for symlinks will + * `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will be skipped. **Default:** `false` * Returns: {Promise} Fulfills with `undefined` upon success. @@ -2103,7 +2103,7 @@ added: v16.7.0 changes: - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/41819 - description: Accepts an additional `verbatimSymlinks` parameter to specify + description: Accepts an additional `verbatimSymlinks` option to specify whether to perform path resolution for symlinks. - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/41678 @@ -2130,7 +2130,7 @@ changes: * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` - * `verbatimSymlinks` {boolean} When `true` path resolution for symlinks will + * `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will be skipped. **Default:** `false` * `callback` {Function} @@ -4893,7 +4893,7 @@ changes: * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` - * `verbatimSymlinks` {boolean} When `true` path resolution for symlinks will + * `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will be skipped. **Default:** `false` Synchronously copies the entire directory structure from `src` to `dest`, From 5d7756aed8082fa5abff5f9316f744b3c54a56e7 Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 19:10:55 +0100 Subject: [PATCH 08/12] Add suggested validations Signed-off-by: Marcos Bjoerkelund --- test/parallel/test-fs-cp.mjs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index 965bab8c4abe9c..6289112bf62a1a 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -98,12 +98,13 @@ function nextdir() { // It throws error when verbatimSymlinks is not a boolean. { const src = './test/fixtures/copy/kitchen-sink'; - [1, [], {}, null, 1n, undefined, null].forEach((verbatimSymlinks) => { - assert.throws( - () => cpSync(src, src, { verbatimSymlinks }), - { code: 'ERR_INVALID_ARG_TYPE' } - ); - }); + [1, [], {}, null, 1n, undefined, null, Symbol(), '', () => {}] + .forEach((verbatimSymlinks) => { + assert.throws( + () => cpSync(src, src, { verbatimSymlinks }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + }); } From 4cd07ac610250887544b44b734a295c14de7c379 Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 19:29:01 +0100 Subject: [PATCH 09/12] options.dereference and options.verbatimSymlink should be mutually exclusive Signed-off-by: Marcos Bjoerkelund --- lib/internal/fs/utils.js | 5 +++++ test/parallel/test-fs-cp.mjs | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index c009c9d7674206..ec56a3e17790fe 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -751,6 +751,11 @@ const validateCpOptions = hideStackFrames((options) => { validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps'); validateBoolean(options.recursive, 'options.recursive'); validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks'); + if (options.dereference === true && options.verbatimSymlinks === true) { + throw new ERR_INVALID_ARG_VALUE('verbatimSymlinks', + options.verbatimSymlinks, + 'cannot be true if dereference is true'); + } if (options.filter !== undefined) { validateFunction(options.filter, 'options.filter'); } diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index 6289112bf62a1a..65e6009fd8afcf 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -108,6 +108,16 @@ function nextdir() { } +// It throws an error when both dereference and verbatimSymlinks are enabled. +{ + const src = './test/fixtures/copy/kitchen-sink'; + assert.throws( + () => cpSync(src, src, { dereference: true, verbatimSymlinks: true }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); +} + + // It resolves relative symlinks to their absolute path by default. { const src = nextdir(); From aa29c91f0b56d60f0cac65c1fdd13cead531bec3 Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Wed, 2 Feb 2022 19:39:32 +0100 Subject: [PATCH 10/12] Fix error type used for tests Signed-off-by: Marcos Bjoerkelund --- test/parallel/test-fs-cp.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index 65e6009fd8afcf..e2e9cec2efac8c 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -113,7 +113,7 @@ function nextdir() { const src = './test/fixtures/copy/kitchen-sink'; assert.throws( () => cpSync(src, src, { dereference: true, verbatimSymlinks: true }), - { code: 'ERR_INVALID_ARG_TYPE' } + { code: 'ERR_INVALID_ARG_VALUE' } ); } From eecce13b7f0736678013d48ace926c69df36e7e7 Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Tue, 8 Feb 2022 14:54:14 +0100 Subject: [PATCH 11/12] Implement feedback: Fix descriptions, use ERR_INCOMPATIBLE_OPTION_PAIR Signed-off-by: Marcos Bjoerkelund --- doc/api/fs.md | 4 +- lib/internal/fs/cp/mv.js | 359 +++++++++++++++++++++++++++++++++++ lib/internal/fs/utils.js | 5 +- test/parallel/test-fs-cp.mjs | 2 +- 4 files changed, 364 insertions(+), 6 deletions(-) create mode 100644 lib/internal/fs/cp/mv.js diff --git a/doc/api/fs.md b/doc/api/fs.md index 33bfafaa438bea..bdaeb1a0fa3854 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -879,7 +879,7 @@ added: v16.7.0 changes: - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/41819 - description: Accepts an additional `verbatimSymlinks` parameter to specify + description: Accepts an additional `verbatimSymlinks` option to specify whether to perform path resolution for symlinks. --> @@ -4872,7 +4872,7 @@ added: v16.7.0 changes: - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/41819 - description: Accepts an additional `verbatimSymlinks` parameter to specify + description: Accepts an additional `verbatimSymlinks` option to specify whether to perform path resolution for symlinks. --> diff --git a/lib/internal/fs/cp/mv.js b/lib/internal/fs/cp/mv.js new file mode 100644 index 00000000000000..8a1a0a66551595 --- /dev/null +++ b/lib/internal/fs/cp/mv.js @@ -0,0 +1,359 @@ +// Copyright 2022 VMware, Inc. +// SPDX-License-Identifier: MIT + +// This file is based on the fs-extra's move method. + +'use strict'; + +const { + ArrayPrototypeEvery, + ArrayPrototypeFilter, + Boolean, + PromiseAll, + PromisePrototypeCatch, + PromisePrototypeThen, + PromiseReject, + SafeArrayIterator, + StringPrototypeSplit, +} = primordials; +const { + rename, + rm, + cp, +} = require('fs/promises'); + +async function mvFn(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_MV_EINVAL({ + message: 'src and dest cannot be the same', + path: dest, + syscall: 'mv', + errno: EINVAL, + code: 'EINVAL', + }); + } + if (srcStat.isDirectory() && !destStat.isDirectory()) { + throw new ERR_FS_MV_DIR_TO_NON_DIR({ + message: `cannot overwrite directory ${src} ` + + `with non-directory ${dest}`, + path: dest, + syscall: 'mv', + errno: EISDIR, + code: 'EISDIR', + }); + } + if (!srcStat.isDirectory() && destStat.isDirectory()) { + throw new ERR_FS_MV_NON_DIR_TO_DIR({ + message: `cannot overwrite non-directory ${src} ` + + `with directory ${dest}`, + path: dest, + syscall: 'mv', + errno: ENOTDIR, + code: 'ENOTDIR', + }); + } + } + + if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { + throw new ERR_FS_MV_EINVAL({ + message: `cannot move ${src} to a subdirectory of self ${dest}`, + path: dest, + syscall: 'mv', + errno: EINVAL, + code: '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: 'mv', + errno: EINVAL, + code: '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: 'mv', + errno: EISDIR, + code: 'EISDIR', + }); + } 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: 'mv', + errno: EINVAL, + code: 'EINVAL', + }); + } else if (srcStat.isFIFO()) { + throw new ERR_FS_CP_FIFO_PIPE({ + message: `cannot copy a FIFO pipe: ${dest}`, + path: dest, + syscall: 'mv', + errno: EINVAL, + code: 'EINVAL', + }); + } + throw new ERR_FS_CP_UNKNOWN({ + message: `cannot copy an unknown file type: ${dest}`, + path: dest, + syscall: 'mv', + errno: EINVAL, + code: '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: 'mv', + errno: EEXIST, + code: '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 opendir(src); + + for await (const { name } of dir) { + const srcItem = join(src, name); + const destItem = join(dest, name); + 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 move ${resolvedSrc} to a subdirectory of self ` + + `${resolvedDest}`, + path: dest, + syscall: 'mv', + errno: EINVAL, + code: 'EINVAL', + }); + } + // Do not move 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: 'mv', + errno: EINVAL, + code: 'EINVAL', + }); + } + return copyLink(resolvedSrc, dest); +} + +async function copyLink(resolvedSrc, dest) { + await unlink(dest); + return symlink(resolvedSrc, dest); +} + +module.exports = { + mvFn, +}; diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index ec56a3e17790fe..481b5292b1d726 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -29,6 +29,7 @@ const { codes: { ERR_FS_EISDIR, ERR_FS_INVALID_SYMLINK_TYPE, + ERR_INCOMPATIBLE_OPTION_PAIR, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_OUT_OF_RANGE @@ -752,9 +753,7 @@ const validateCpOptions = hideStackFrames((options) => { validateBoolean(options.recursive, 'options.recursive'); validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks'); if (options.dereference === true && options.verbatimSymlinks === true) { - throw new ERR_INVALID_ARG_VALUE('verbatimSymlinks', - options.verbatimSymlinks, - 'cannot be true if dereference is true'); + throw new ERR_INCOMPATIBLE_OPTION_PAIR('dereference', 'verbatimSymlinks'); } if (options.filter !== undefined) { validateFunction(options.filter, 'options.filter'); diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index e2e9cec2efac8c..dfe6254c6b6494 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -113,7 +113,7 @@ function nextdir() { const src = './test/fixtures/copy/kitchen-sink'; assert.throws( () => cpSync(src, src, { dereference: true, verbatimSymlinks: true }), - { code: 'ERR_INVALID_ARG_VALUE' } + { code: 'ERR_INCOMPATIBLE_OPTION_PAIR' } ); } From 7527fdcb4f3e7e9f4980379727e5e14655b8f04f Mon Sep 17 00:00:00 2001 From: Marcos Bjoerkelund Date: Tue, 8 Feb 2022 14:56:52 +0100 Subject: [PATCH 12/12] Remove unrelated file Signed-off-by: Marcos Bjoerkelund --- lib/internal/fs/cp/mv.js | 359 --------------------------------------- 1 file changed, 359 deletions(-) delete mode 100644 lib/internal/fs/cp/mv.js diff --git a/lib/internal/fs/cp/mv.js b/lib/internal/fs/cp/mv.js deleted file mode 100644 index 8a1a0a66551595..00000000000000 --- a/lib/internal/fs/cp/mv.js +++ /dev/null @@ -1,359 +0,0 @@ -// Copyright 2022 VMware, Inc. -// SPDX-License-Identifier: MIT - -// This file is based on the fs-extra's move method. - -'use strict'; - -const { - ArrayPrototypeEvery, - ArrayPrototypeFilter, - Boolean, - PromiseAll, - PromisePrototypeCatch, - PromisePrototypeThen, - PromiseReject, - SafeArrayIterator, - StringPrototypeSplit, -} = primordials; -const { - rename, - rm, - cp, -} = require('fs/promises'); - -async function mvFn(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_MV_EINVAL({ - message: 'src and dest cannot be the same', - path: dest, - syscall: 'mv', - errno: EINVAL, - code: 'EINVAL', - }); - } - if (srcStat.isDirectory() && !destStat.isDirectory()) { - throw new ERR_FS_MV_DIR_TO_NON_DIR({ - message: `cannot overwrite directory ${src} ` + - `with non-directory ${dest}`, - path: dest, - syscall: 'mv', - errno: EISDIR, - code: 'EISDIR', - }); - } - if (!srcStat.isDirectory() && destStat.isDirectory()) { - throw new ERR_FS_MV_NON_DIR_TO_DIR({ - message: `cannot overwrite non-directory ${src} ` + - `with directory ${dest}`, - path: dest, - syscall: 'mv', - errno: ENOTDIR, - code: 'ENOTDIR', - }); - } - } - - if (srcStat.isDirectory() && isSrcSubdir(src, dest)) { - throw new ERR_FS_MV_EINVAL({ - message: `cannot move ${src} to a subdirectory of self ${dest}`, - path: dest, - syscall: 'mv', - errno: EINVAL, - code: '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: 'mv', - errno: EINVAL, - code: '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: 'mv', - errno: EISDIR, - code: 'EISDIR', - }); - } 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: 'mv', - errno: EINVAL, - code: 'EINVAL', - }); - } else if (srcStat.isFIFO()) { - throw new ERR_FS_CP_FIFO_PIPE({ - message: `cannot copy a FIFO pipe: ${dest}`, - path: dest, - syscall: 'mv', - errno: EINVAL, - code: 'EINVAL', - }); - } - throw new ERR_FS_CP_UNKNOWN({ - message: `cannot copy an unknown file type: ${dest}`, - path: dest, - syscall: 'mv', - errno: EINVAL, - code: '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: 'mv', - errno: EEXIST, - code: '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 opendir(src); - - for await (const { name } of dir) { - const srcItem = join(src, name); - const destItem = join(dest, name); - 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 move ${resolvedSrc} to a subdirectory of self ` + - `${resolvedDest}`, - path: dest, - syscall: 'mv', - errno: EINVAL, - code: 'EINVAL', - }); - } - // Do not move 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: 'mv', - errno: EINVAL, - code: 'EINVAL', - }); - } - return copyLink(resolvedSrc, dest); -} - -async function copyLink(resolvedSrc, dest) { - await unlink(dest); - return symlink(resolvedSrc, dest); -} - -module.exports = { - mvFn, -};