From a54e898dc823d5ae489a3b9deb33b433e14d4b2d Mon Sep 17 00:00:00 2001 From: Tetsuharu Ohzeki <tetsuharu.ohzeki@gmail.com> Date: Thu, 20 Apr 2023 15:28:18 +0900 Subject: [PATCH] fs: add support for mode flag to specify the copy behavior `fs.copyFile()` supports copy-on-write operation if the underlying platform supports it by passing a mode flag. This behavior was added in https://github.com/nodejs/node/commit/a16d88d9e9a6581e463082549823189aba25ca76. This patch adds `mode` flag to `fs.cp()`, `fs.cpSync()`, and `fsPromises.cp()` to allow to change their behaviors to copy files. This test case is based on the test case that was introduced when we add `fs.constants.COPYFILE_FICLONE`. https://github.com/nodejs/node/commit/a16d88d9e9a6581e463082549823189aba25ca76. This test strategy is: - If the platform supports copy-on-write operation, check whether the destination is expected - Otherwise, the operation will fail and check whether the failure error information is expected. Fixes: https://github.com/nodejs/node/issues/47080 PR-URL: https://github.com/nodejs/node/pull/47084 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> --- doc/api/fs.md | 20 +++++++ lib/internal/fs/cp/cp-sync.js | 2 +- lib/internal/fs/cp/cp.js | 2 +- lib/internal/fs/utils.js | 1 + test/parallel/test-fs-cp.mjs | 104 ++++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 2 deletions(-) diff --git a/doc/api/fs.md b/doc/api/fs.md index 810334aeed2e6a..c4446522537b2c 100644 --- a/doc/api/fs.md +++ b/doc/api/fs.md @@ -966,6 +966,10 @@ try { <!-- YAML added: v16.7.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/47084 + description: Accept an additional `mode` option to specify + the copy behavior as the `mode` argument of `fs.copyFile()`. - version: - v17.6.0 - v16.15.0 @@ -992,6 +996,8 @@ changes: operation will ignore errors if you set this to false and the destination exists. Use the `errorOnExist` option to change this behavior. **Default:** `true`. + * `mode` {integer} modifiers for copy operation. **Default:** `0`. + See `mode` flag of [`fsPromises.copyFile()`][]. * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` @@ -2286,6 +2292,10 @@ copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback); <!-- YAML added: v16.7.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/47084 + description: Accept an additional `mode` option to specify + the copy behavior as the `mode` argument of `fs.copyFile()`. - version: v18.0.0 pr-url: https://github.com/nodejs/node/pull/41678 description: Passing an invalid callback to the `callback` argument @@ -2317,6 +2327,8 @@ changes: operation will ignore errors if you set this to false and the destination exists. Use the `errorOnExist` option to change this behavior. **Default:** `true`. + * `mode` {integer} modifiers for copy operation. **Default:** `0`. + See `mode` flag of [`fs.copyFile()`][]. * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` @@ -5191,6 +5203,10 @@ copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL); <!-- YAML added: v16.7.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/47084 + description: Accept an additional `mode` option to specify + the copy behavior as the `mode` argument of `fs.copyFile()`. - version: - v17.6.0 - v16.15.0 @@ -5216,6 +5232,8 @@ changes: operation will ignore errors if you set this to false and the destination exists. Use the `errorOnExist` option to change this behavior. **Default:** `true`. + * `mode` {integer} modifiers for copy operation. **Default:** `0`. + See `mode` flag of [`fs.copyFileSync()`][]. * `preserveTimestamps` {boolean} When `true` timestamps from `src` will be preserved. **Default:** `false`. * `recursive` {boolean} copy directories recursively **Default:** `false` @@ -8004,6 +8022,7 @@ the file contents. [`fs.chmod()`]: #fschmodpath-mode-callback [`fs.chown()`]: #fschownpath-uid-gid-callback [`fs.copyFile()`]: #fscopyfilesrc-dest-mode-callback +[`fs.copyFileSync()`]: #fscopyfilesyncsrc-dest-mode [`fs.createReadStream()`]: #fscreatereadstreampath-options [`fs.createWriteStream()`]: #fscreatewritestreampath-options [`fs.exists()`]: #fsexistspath-callback @@ -8037,6 +8056,7 @@ the file contents. [`fs.writeFile()`]: #fswritefilefile-data-options-callback [`fs.writev()`]: #fswritevfd-buffers-position-callback [`fsPromises.access()`]: #fspromisesaccesspath-mode +[`fsPromises.copyFile()`]: #fspromisescopyfilesrc-dest-mode [`fsPromises.open()`]: #fspromisesopenpath-flags-mode [`fsPromises.opendir()`]: #fspromisesopendirpath-options [`fsPromises.rm()`]: #fspromisesrmpath-options diff --git a/lib/internal/fs/cp/cp-sync.js b/lib/internal/fs/cp/cp-sync.js index cfd54a4ff0e23f..348d45adcd7c4e 100644 --- a/lib/internal/fs/cp/cp-sync.js +++ b/lib/internal/fs/cp/cp-sync.js @@ -226,7 +226,7 @@ function mayCopyFile(srcStat, src, dest, opts) { } function copyFile(srcStat, src, dest, opts) { - copyFileSync(src, dest); + copyFileSync(src, dest, opts.mode); if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest); return setDestMode(dest, srcStat.mode); } diff --git a/lib/internal/fs/cp/cp.js b/lib/internal/fs/cp/cp.js index cba405223a4fc7..130bf98ae7be4c 100644 --- a/lib/internal/fs/cp/cp.js +++ b/lib/internal/fs/cp/cp.js @@ -257,7 +257,7 @@ async function mayCopyFile(srcStat, src, dest, opts) { } async function _copyFile(srcStat, src, dest, opts) { - await copyFile(src, dest); + await copyFile(src, dest, opts.mode); if (opts.preserveTimestamps) { return handleTimestampsAndMode(srcStat.mode, src, dest); } diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index b32527f9cdf779..c4f4785b339c39 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -787,6 +787,7 @@ const validateCpOptions = hideStackFrames((options) => { validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps'); validateBoolean(options.recursive, 'options.recursive'); validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks'); + options.mode = getValidMode(options.mode, 'copyFile'); if (options.dereference === true && options.verbatimSymlinks === true) { throw new ERR_INCOMPATIBLE_OPTION_PAIR('dereference', 'verbatimSymlinks'); } diff --git a/test/parallel/test-fs-cp.mjs b/test/parallel/test-fs-cp.mjs index c4d6c4b737e371..8aedad334d2b24 100644 --- a/test/parallel/test-fs-cp.mjs +++ b/test/parallel/test-fs-cp.mjs @@ -38,6 +38,30 @@ function nextdir() { assertDirEquivalent(src, dest); } +// It copies a nested folder structure with mode flags. +// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`. +(() => { + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + try { + cpSync(src, dest, mustNotMutateObjectDeep({ + recursive: true, + mode: fs.constants.COPYFILE_FICLONE_FORCE, + })); + } catch (err) { + // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, + // it should enter this path. + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); + return; + } + + // If the platform support `COPYFILE_FICLONE_FORCE` operation, + // it should reach to here. + assertDirEquivalent(src, dest); +})(); + // It does not throw errors when directory is copied over and force is false. { const src = nextdir(); @@ -107,6 +131,14 @@ function nextdir() { }); } +// It rejects if options.mode is invalid. +{ + assert.throws( + () => cpSync('a', 'b', { mode: -1 }), + { code: 'ERR_OUT_OF_RANGE' } + ); +} + // It throws an error when both dereference and verbatimSymlinks are enabled. { @@ -425,6 +457,31 @@ if (!isWindows) { })); } +// It copies a nested folder structure with mode flags. +// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + cp(src, dest, mustNotMutateObjectDeep({ + recursive: true, + mode: fs.constants.COPYFILE_FICLONE_FORCE, + }), mustCall((err) => { + if (!err) { + // If the platform support `COPYFILE_FICLONE_FORCE` operation, + // it should reach to here. + assert.strictEqual(err, null); + assertDirEquivalent(src, dest); + return; + } + + // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, + // it should enter this path. + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); + })); +} + // It does not throw errors when directory is copied over and force is false. { const src = nextdir(); @@ -799,6 +856,14 @@ if (!isWindows) { ); } +// It throws if options is not object. +{ + assert.throws( + () => cp('a', 'b', { mode: -1 }, () => {}), + { code: 'ERR_OUT_OF_RANGE' } + ); +} + // Promises implementation of copy. // It copies a nested folder structure with files and folders. @@ -810,6 +875,35 @@ if (!isWindows) { assertDirEquivalent(src, dest); } +// It copies a nested folder structure with mode flags. +// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`. +{ + const src = './test/fixtures/copy/kitchen-sink'; + const dest = nextdir(); + let p = null; + let successFiClone = false; + try { + p = await fs.promises.cp(src, dest, mustNotMutateObjectDeep({ + recursive: true, + mode: fs.constants.COPYFILE_FICLONE_FORCE, + })); + successFiClone = true; + } catch (err) { + // If the platform does not support `COPYFILE_FICLONE_FORCE` operation, + // it should enter this path. + assert.strictEqual(err.syscall, 'copyfile'); + assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' || + err.code === 'ENOSYS' || err.code === 'EXDEV'); + } + + if (successFiClone) { + // If the platform support `COPYFILE_FICLONE_FORCE` operation, + // it should reach to here. + assert.strictEqual(p, undefined); + assertDirEquivalent(src, dest); + } +} + // It accepts file URL as src and dest. { const src = './test/fixtures/copy/kitchen-sink'; @@ -847,6 +941,16 @@ if (!isWindows) { ); } +// It rejects if options.mode is invalid. +{ + await assert.rejects( + fs.promises.cp('a', 'b', { + mode: -1, + }), + { code: 'ERR_OUT_OF_RANGE' } + ); +} + function assertDirEquivalent(dir1, dir2) { const dir1Entries = []; collectEntries(dir1, dir1Entries);