Skip to content

Commit 3f0636d

Browse files
tetsuharuohzekiMoLow
authored andcommitted
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 a16d88d. 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`. a16d88d. 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: #47080 PR-URL: #47084 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 6ab8927 commit 3f0636d

File tree

5 files changed

+128
-3
lines changed

5 files changed

+128
-3
lines changed

doc/api/fs.md

+21-1
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,10 @@ try {
962962
<!-- YAML
963963
added: v16.7.0
964964
changes:
965+
- version: REPLACEME
966+
pr-url: https://github.com/nodejs/node/pull/47084
967+
description: Accept an additional `mode` option to specify
968+
the copy behavior as the `mode` argument of `fs.copyFile()`.
965969
- version: v17.6.0
966970
pr-url: https://github.com/nodejs/node/pull/41819
967971
description: Accepts an additional `verbatimSymlinks` option to specify
@@ -986,6 +990,8 @@ changes:
986990
operation will ignore errors if you set this to false and the destination
987991
exists. Use the `errorOnExist` option to change this behavior.
988992
**Default:** `true`.
993+
* `mode` {integer} modifiers for copy operation. **Default:** `0`.
994+
See `mode` flag of [`fsPromises.copyFile()`][].
989995
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
990996
be preserved. **Default:** `false`.
991997
* `recursive` {boolean} copy directories recursively **Default:** `false`
@@ -2269,6 +2275,10 @@ copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback);
22692275
<!-- YAML
22702276
added: v16.7.0
22712277
changes:
2278+
- version: REPLACEME
2279+
pr-url: https://github.com/nodejs/node/pull/47084
2280+
description: Accept an additional `mode` option to specify
2281+
the copy behavior as the `mode` argument of `fs.copyFile()`.
22722282
- version: v18.0.0
22732283
pr-url: https://github.com/nodejs/node/pull/41678
22742284
description: Passing an invalid callback to the `callback` argument
@@ -2298,6 +2308,8 @@ changes:
22982308
operation will ignore errors if you set this to false and the destination
22992309
exists. Use the `errorOnExist` option to change this behavior.
23002310
**Default:** `true`.
2311+
* `mode` {integer} modifiers for copy operation. **Default:** `0`.
2312+
See `mode` flag of [`fs.copyFile()`][].
23012313
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
23022314
be preserved. **Default:** `false`.
23032315
* `recursive` {boolean} copy directories recursively **Default:** `false`
@@ -5120,7 +5132,11 @@ copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL);
51205132
<!-- YAML
51215133
added: v16.7.0
51225134
changes:
5123-
- version: v17.6.0
5135+
- version: REPLACEME
5136+
pr-url: https://github.com/nodejs/node/pull/47084
5137+
description: Accept an additional `mode` option to specify
5138+
the copy behavior as the `mode` argument of `fs.copyFile()`.
5139+
- version: v17.6.0
51245140
pr-url: https://github.com/nodejs/node/pull/41819
51255141
description: Accepts an additional `verbatimSymlinks` option to specify
51265142
whether to perform path resolution for symlinks.
@@ -5143,6 +5159,8 @@ changes:
51435159
operation will ignore errors if you set this to false and the destination
51445160
exists. Use the `errorOnExist` option to change this behavior.
51455161
**Default:** `true`.
5162+
* `mode` {integer} modifiers for copy operation. **Default:** `0`.
5163+
See `mode` flag of [`fs.copyFileSync()`][].
51465164
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
51475165
be preserved. **Default:** `false`.
51485166
* `recursive` {boolean} copy directories recursively **Default:** `false`
@@ -7907,6 +7925,7 @@ the file contents.
79077925
[`fs.chmod()`]: #fschmodpath-mode-callback
79087926
[`fs.chown()`]: #fschownpath-uid-gid-callback
79097927
[`fs.copyFile()`]: #fscopyfilesrc-dest-mode-callback
7928+
[`fs.copyFileSync()`]: #fscopyfilesyncsrc-dest-mode
79107929
[`fs.createReadStream()`]: #fscreatereadstreampath-options
79117930
[`fs.createWriteStream()`]: #fscreatewritestreampath-options
79127931
[`fs.exists()`]: #fsexistspath-callback
@@ -7940,6 +7959,7 @@ the file contents.
79407959
[`fs.writeFile()`]: #fswritefilefile-data-options-callback
79417960
[`fs.writev()`]: #fswritevfd-buffers-position-callback
79427961
[`fsPromises.access()`]: #fspromisesaccesspath-mode
7962+
[`fsPromises.copyFile()`]: #fspromisescopyfilesrc-dest-mode
79437963
[`fsPromises.open()`]: #fspromisesopenpath-flags-mode
79447964
[`fsPromises.opendir()`]: #fspromisesopendirpath-options
79457965
[`fsPromises.rm()`]: #fspromisesrmpath-options

lib/internal/fs/cp/cp-sync.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ function mayCopyFile(srcStat, src, dest, opts) {
226226
}
227227

228228
function copyFile(srcStat, src, dest, opts) {
229-
copyFileSync(src, dest);
229+
copyFileSync(src, dest, opts.mode);
230230
if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest);
231231
return setDestMode(dest, srcStat.mode);
232232
}

lib/internal/fs/cp/cp.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ async function mayCopyFile(srcStat, src, dest, opts) {
257257
}
258258

259259
async function _copyFile(srcStat, src, dest, opts) {
260-
await copyFile(src, dest);
260+
await copyFile(src, dest, opts.mode);
261261
if (opts.preserveTimestamps) {
262262
return handleTimestampsAndMode(srcStat.mode, src, dest);
263263
}

lib/internal/fs/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,7 @@ const validateCpOptions = hideStackFrames((options) => {
774774
validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps');
775775
validateBoolean(options.recursive, 'options.recursive');
776776
validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks');
777+
options.mode = getValidMode(options.mode, 'copyFile');
777778
if (options.dereference === true && options.verbatimSymlinks === true) {
778779
throw new ERR_INCOMPATIBLE_OPTION_PAIR('dereference', 'verbatimSymlinks');
779780
}

test/parallel/test-fs-cp.mjs

+104
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@ function nextdir() {
3838
assertDirEquivalent(src, dest);
3939
}
4040

41+
// It copies a nested folder structure with mode flags.
42+
// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`.
43+
(() => {
44+
const src = './test/fixtures/copy/kitchen-sink';
45+
const dest = nextdir();
46+
try {
47+
cpSync(src, dest, mustNotMutateObjectDeep({
48+
recursive: true,
49+
mode: fs.constants.COPYFILE_FICLONE_FORCE,
50+
}));
51+
} catch (err) {
52+
// If the platform does not support `COPYFILE_FICLONE_FORCE` operation,
53+
// it should enter this path.
54+
assert.strictEqual(err.syscall, 'copyfile');
55+
assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' ||
56+
err.code === 'ENOSYS' || err.code === 'EXDEV');
57+
return;
58+
}
59+
60+
// If the platform support `COPYFILE_FICLONE_FORCE` operation,
61+
// it should reach to here.
62+
assertDirEquivalent(src, dest);
63+
})();
64+
4165
// It does not throw errors when directory is copied over and force is false.
4266
{
4367
const src = nextdir();
@@ -107,6 +131,14 @@ function nextdir() {
107131
});
108132
}
109133

134+
// It rejects if options.mode is invalid.
135+
{
136+
assert.throws(
137+
() => cpSync('a', 'b', { mode: -1 }),
138+
{ code: 'ERR_OUT_OF_RANGE' }
139+
);
140+
}
141+
110142

111143
// It throws an error when both dereference and verbatimSymlinks are enabled.
112144
{
@@ -425,6 +457,31 @@ if (!isWindows) {
425457
}));
426458
}
427459

460+
// It copies a nested folder structure with mode flags.
461+
// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`.
462+
{
463+
const src = './test/fixtures/copy/kitchen-sink';
464+
const dest = nextdir();
465+
cp(src, dest, mustNotMutateObjectDeep({
466+
recursive: true,
467+
mode: fs.constants.COPYFILE_FICLONE_FORCE,
468+
}), mustCall((err) => {
469+
if (!err) {
470+
// If the platform support `COPYFILE_FICLONE_FORCE` operation,
471+
// it should reach to here.
472+
assert.strictEqual(err, null);
473+
assertDirEquivalent(src, dest);
474+
return;
475+
}
476+
477+
// If the platform does not support `COPYFILE_FICLONE_FORCE` operation,
478+
// it should enter this path.
479+
assert.strictEqual(err.syscall, 'copyfile');
480+
assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' ||
481+
err.code === 'ENOSYS' || err.code === 'EXDEV');
482+
}));
483+
}
484+
428485
// It does not throw errors when directory is copied over and force is false.
429486
{
430487
const src = nextdir();
@@ -799,6 +856,14 @@ if (!isWindows) {
799856
);
800857
}
801858

859+
// It throws if options is not object.
860+
{
861+
assert.throws(
862+
() => cp('a', 'b', { mode: -1 }, () => {}),
863+
{ code: 'ERR_OUT_OF_RANGE' }
864+
);
865+
}
866+
802867
// Promises implementation of copy.
803868

804869
// It copies a nested folder structure with files and folders.
@@ -810,6 +875,35 @@ if (!isWindows) {
810875
assertDirEquivalent(src, dest);
811876
}
812877

878+
// It copies a nested folder structure with mode flags.
879+
// This test is based on fs.promises.copyFile() with `COPYFILE_FICLONE_FORCE`.
880+
{
881+
const src = './test/fixtures/copy/kitchen-sink';
882+
const dest = nextdir();
883+
let p = null;
884+
let successFiClone = false;
885+
try {
886+
p = await fs.promises.cp(src, dest, mustNotMutateObjectDeep({
887+
recursive: true,
888+
mode: fs.constants.COPYFILE_FICLONE_FORCE,
889+
}));
890+
successFiClone = true;
891+
} catch (err) {
892+
// If the platform does not support `COPYFILE_FICLONE_FORCE` operation,
893+
// it should enter this path.
894+
assert.strictEqual(err.syscall, 'copyfile');
895+
assert(err.code === 'ENOTSUP' || err.code === 'ENOTTY' ||
896+
err.code === 'ENOSYS' || err.code === 'EXDEV');
897+
}
898+
899+
if (successFiClone) {
900+
// If the platform support `COPYFILE_FICLONE_FORCE` operation,
901+
// it should reach to here.
902+
assert.strictEqual(p, undefined);
903+
assertDirEquivalent(src, dest);
904+
}
905+
}
906+
813907
// It accepts file URL as src and dest.
814908
{
815909
const src = './test/fixtures/copy/kitchen-sink';
@@ -847,6 +941,16 @@ if (!isWindows) {
847941
);
848942
}
849943

944+
// It rejects if options.mode is invalid.
945+
{
946+
await assert.rejects(
947+
fs.promises.cp('a', 'b', {
948+
mode: -1,
949+
}),
950+
{ code: 'ERR_OUT_OF_RANGE' }
951+
);
952+
}
953+
850954
function assertDirEquivalent(dir1, dir2) {
851955
const dir1Entries = [];
852956
collectEntries(dir1, dir1Entries);

0 commit comments

Comments
 (0)