Skip to content

Commit a54e898

Browse files
tetsuharuohzekitargos
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 cfafe43 commit a54e898

5 files changed

Lines changed: 127 additions & 2 deletions

File tree

‎doc/api/fs.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,10 @@ try {
966966
<!-- YAML
967967
added: v16.7.0
968968
changes:
969+
- version: REPLACEME
970+
pr-url: https://github.com/nodejs/node/pull/47084
971+
description: Accept an additional `mode` option to specify
972+
the copy behavior as the `mode` argument of `fs.copyFile()`.
969973
- version:
970974
- v17.6.0
971975
- v16.15.0
@@ -992,6 +996,8 @@ changes:
992996
operation will ignore errors if you set this to false and the destination
993997
exists. Use the `errorOnExist` option to change this behavior.
994998
**Default:** `true`.
999+
* `mode` {integer} modifiers for copy operation. **Default:** `0`.
1000+
See `mode` flag of [`fsPromises.copyFile()`][].
9951001
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
9961002
be preserved. **Default:** `false`.
9971003
* `recursive` {boolean} copy directories recursively **Default:** `false`
@@ -2286,6 +2292,10 @@ copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback);
22862292
<!-- YAML
22872293
added: v16.7.0
22882294
changes:
2295+
- version: REPLACEME
2296+
pr-url: https://github.com/nodejs/node/pull/47084
2297+
description: Accept an additional `mode` option to specify
2298+
the copy behavior as the `mode` argument of `fs.copyFile()`.
22892299
- version: v18.0.0
22902300
pr-url: https://github.com/nodejs/node/pull/41678
22912301
description: Passing an invalid callback to the `callback` argument
@@ -2317,6 +2327,8 @@ changes:
23172327
operation will ignore errors if you set this to false and the destination
23182328
exists. Use the `errorOnExist` option to change this behavior.
23192329
**Default:** `true`.
2330+
* `mode` {integer} modifiers for copy operation. **Default:** `0`.
2331+
See `mode` flag of [`fs.copyFile()`][].
23202332
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
23212333
be preserved. **Default:** `false`.
23222334
* `recursive` {boolean} copy directories recursively **Default:** `false`
@@ -5191,6 +5203,10 @@ copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL);
51915203
<!--YAML
51925204
added: v16.7.0
51935205
changes:
5206+
- version:REPLACEME
5207+
pr-url: https://github.com/nodejs/node/pull/47084
5208+
description: Accept an additional `mode` option to specify
5209+
the copy behavior as the `mode` argument of`fs.copyFile()`.
51945210
- version:
51955211
- v17.6.0
51965212
- v16.15.0
@@ -5216,6 +5232,8 @@ changes:
52165232
operation will ignore errors if you set this to false and the destination
52175233
exists. Use the `errorOnExist` option to change this behavior.
52185234
**Default:**`true`.
5235+
*`mode` {integer} modifiers for copy operation. **Default:**`0`.
5236+
See `mode` flag of [`fs.copyFileSync()`][].
52195237
*`preserveTimestamps` {boolean} When `true` timestamps from `src` will
52205238
be preserved. **Default:**`false`.
52215239
*`recursive` {boolean} copy directories recursively **Default:**`false`
@@ -8004,6 +8022,7 @@ the file contents.
80048022
[`fs.chmod()`]: #fschmodpath-mode-callback
80058023
[`fs.chown()`]: #fschownpath-uid-gid-callback
80068024
[`fs.copyFile()`]: #fscopyfilesrc-dest-mode-callback
8025+
[`fs.copyFileSync()`]: #fscopyfilesyncsrc-dest-mode
80078026
[`fs.createReadStream()`]: #fscreatereadstreampath-options
80088027
[`fs.createWriteStream()`]: #fscreatewritestreampath-options
80098028
[`fs.exists()`]: #fsexistspath-callback
@@ -8037,6 +8056,7 @@ the file contents.
80378056
[`fs.writeFile()`]: #fswritefilefile-data-options-callback
80388057
[`fs.writev()`]: #fswritevfd-buffers-position-callback
80398058
[`fsPromises.access()`]: #fspromisesaccesspath-mode
8059+
[`fsPromises.copyFile()`]: #fspromisescopyfilesrc-dest-mode
80408060
[`fsPromises.open()`]: #fspromisesopenpath-flags-mode
80418061
[`fsPromises.opendir()`]: #fspromisesopendirpath-options
80428062
[`fsPromises.rm()`]: #fspromisesrmpath-options

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

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

228228
functioncopyFile(srcStat,src,dest,opts){
229-
copyFileSync(src,dest);
229+
copyFileSync(src,dest,opts.mode);
230230
if(opts.preserveTimestamps)handleTimestamps(srcStat.mode,src,dest);
231231
returnsetDestMode(dest,srcStat.mode);
232232
}

‎lib/internal/fs/cp/cp.js‎

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

259259
asyncfunction_copyFile(srcStat,src,dest,opts){
260-
awaitcopyFile(src,dest);
260+
awaitcopyFile(src,dest,opts.mode);
261261
if(opts.preserveTimestamps){
262262
returnhandleTimestampsAndMode(srcStat.mode,src,dest);
263263
}

‎lib/internal/fs/utils.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@ const validateCpOptions = hideStackFrames((options) => {
787787
validateBoolean(options.preserveTimestamps,'options.preserveTimestamps');
788788
validateBoolean(options.recursive,'options.recursive');
789789
validateBoolean(options.verbatimSymlinks,'options.verbatimSymlinks');
790+
options.mode=getValidMode(options.mode,'copyFile');
790791
if(options.dereference===true&&options.verbatimSymlinks===true){
791792
thrownewERR_INCOMPATIBLE_OPTION_PAIR('dereference','verbatimSymlinks');
792793
}

‎test/parallel/test-fs-cp.mjs‎

Lines changed: 104 additions & 0 deletions
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+
constsrc='./test/fixtures/copy/kitchen-sink';
45+
constdest=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
constsrc=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+
constsrc='./test/fixtures/copy/kitchen-sink';
464+
constdest=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
constsrc=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+
constsrc='./test/fixtures/copy/kitchen-sink';
882+
constdest=nextdir();
883+
letp=null;
884+
letsuccessFiClone=false;
885+
try{
886+
p=awaitfs.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
constsrc='./test/fixtures/copy/kitchen-sink';
@@ -847,6 +941,16 @@ if (!isWindows) {
847941
);
848942
}
849943

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

0 commit comments

Comments
 (0)