Skip to content

Commit 0dc167a

Browse files
bcoedanielleadams
authored andcommitted
fs: add recursive cp method
Introduces recursive cp method, based on fs-extra implementation. PR-URL: #39372 Fixes: #35880 Refs: nodejs/tooling#98 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ian Sutherland <[email protected]>
1 parent 103bf20 commit 0dc167a

File tree

20 files changed

+1782
-1
lines changed

20 files changed

+1782
-1
lines changed

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,3 +1584,22 @@ The externally maintained libraries used by Node.js are:
15841584
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
15851585
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15861586
"""
1587+
1588+
- node-fs-extra, located at lib/internal/fs/cp, is licensed as follows:
1589+
"""
1590+
(The MIT License)
1591+
1592+
Copyright (c) 2011-2017 JP Richardson
1593+
1594+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
1595+
(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
1596+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
1597+
furnished to do so, subject to the following conditions:
1598+
1599+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
1600+
1601+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
1602+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
1603+
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1604+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1605+
"""

doc/api/errors.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,74 @@ added: v14.0.0
11071107
Used when a feature that is not available
11081108
to the current platform which is running Node.js is used.
11091109

1110+
<a id="ERR_FS_CP_DIR_TO_NON_DIR"></a>
1111+
### `ERR_FS_CP_DIR_TO_NON_DIR`
1112+
<!--
1113+
added: REPLACEME
1114+
-->
1115+
1116+
An attempt was made to copy a directory to a non-directory (file, symlink,
1117+
etc.) using [`fs.cp()`][].
1118+
1119+
<a id="ERR_FS_CP_EEXIST"></a>
1120+
### `ERR_FS_CP_EEXIST`
1121+
<!--
1122+
added: REPLACEME
1123+
-->
1124+
1125+
An attempt was made to copy over a file that already existed with
1126+
[`fs.cp()`][], with the `force` and `errorOnExist` set to `true`.
1127+
1128+
<a id="ERR_FS_CP_EINVAL"></a>
1129+
### `ERR_FS_CP_EINVAL`
1130+
<!--
1131+
added: REPLACEME
1132+
-->
1133+
1134+
When using [`fs.cp()`][], `src` or `dest` pointed to an invalid path.
1135+
1136+
<a id="ERR_FS_CP_FIFO_PIPE"></a>
1137+
### `ERR_FS_CP_FIFO_PIPE`
1138+
<!--
1139+
added: REPLACEME
1140+
-->
1141+
1142+
An attempt was made to copy a named pipe with [`fs.cp()`][].
1143+
1144+
<a id="ERR_FS_CP_NON_DIR_TO_DIR"></a>
1145+
### `ERR_FS_CP_NON_DIR_TO_DIR`
1146+
<!--
1147+
added: REPLACEME
1148+
-->
1149+
1150+
An attempt was made to copy a non-directory (file, symlink, etc.) to a directory
1151+
using [`fs.cp()`][].
1152+
1153+
<a id="ERR_FS_CP_SOCKET"></a>
1154+
### `ERR_FS_CP_SOCKET`
1155+
<!--
1156+
added: REPLACEME
1157+
-->
1158+
1159+
An attempt was made to copy to a socket with [`fs.cp()`][].
1160+
1161+
<a id="ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY"></a>
1162+
### `ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY`
1163+
<!--
1164+
added: REPLACEME
1165+
-->
1166+
1167+
When using [`fs.cp()`][], a symlink in `dest` pointed to a subdirectory
1168+
of `src`.
1169+
1170+
<a id="ERR_FS_CP_UNKNOWN"></a>
1171+
### `ERR_FS_CP_UNKNOWN`
1172+
<!--
1173+
added: REPLACEME
1174+
-->
1175+
1176+
An attempt was made to copy to an unknown file type with [`fs.cp()`][].
1177+
11101178
<a id="ERR_FS_EISDIR"></a>
11111179
### `ERR_FS_EISDIR`
11121180

@@ -2814,6 +2882,7 @@ The native call from `process.cpuUsage` could not be processed.
28142882
[`dgram.remoteAddress()`]: dgram.md#dgram_socket_remoteaddress
28152883
[`errno`(3) man page]: https://man7.org/linux/man-pages/man3/errno.3.html
28162884
[`fs.Dir`]: fs.md#fs_class_fs_dir
2885+
[`fs.cp()`]: fs.md#fs_fs_cp_src_dest_options_callback
28172886
[`fs.readFileSync`]: fs.md#fs_fs_readfilesync_path_options
28182887
[`fs.readdir`]: fs.md#fs_fs_readdir_path_options_callback
28192888
[`fs.symlink()`]: fs.md#fs_fs_symlink_target_path_type_callback

doc/api/fs.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,37 @@ try {
693693
}
694694
```
695695
696+
### `fsPromises.cp(src, dest[, options])`
697+
<!-- YAML
698+
added: REPLACEME
699+
-->
700+
701+
> Stability: 1 - Experimental
702+
703+
* `src` {string|URL} source path to copy.
704+
* `dest` {string|URL} destination path to copy to.
705+
* `options` {Object}
706+
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
707+
* `errorOnExist` {boolean} when `force` is `false`, and the destination
708+
exists, throw an error. **Default:** `false`.
709+
* `filter` {Function} Function to filter copied files/directories. Return
710+
`true` to copy the item, `false` to ignore it. Can also return a `Promise`
711+
that resolves to `true` or `false` **Default:** `undefined`.
712+
* `force` {boolean} overwrite existing file or directory. _The copy
713+
operation will ignore errors if you set this to false and the destination
714+
exists. Use the `errorOnExist` option to change this behavior.
715+
**Default:** `true`.
716+
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
717+
be preserved. **Default:** `false`.
718+
* `recursive` {boolean} copy directories recursively **Default:** `false`
719+
* Returns: {Promise} Fulfills with `undefined` upon success.
720+
721+
Asynchronously copies the entire directory structure from `src` to `dest`,
722+
including subdirectories and files.
723+
724+
When copying a directory to another directory, globs are not supported and
725+
behavior is similar to `cp dir1/ dir2/`.
726+
696727
### `fsPromises.lchmod(path, mode)`
697728
<!-- YAML
698729
deprecated: v10.0.0
@@ -1800,6 +1831,37 @@ copyFile('source.txt', 'destination.txt', callback);
18001831
copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback);
18011832
```
18021833
1834+
### `fs.cp(src, dest[, options], callback)`
1835+
<!-- YAML
1836+
added: REPLACEME
1837+
-->
1838+
1839+
> Stability: 1 - Experimental
1840+
1841+
* `src` {string|URL} source path to copy.
1842+
* `dest` {string|URL} destination path to copy to.
1843+
* `options` {Object}
1844+
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
1845+
* `errorOnExist` {boolean} when `force` is `false`, and the destination
1846+
exists, throw an error. **Default:** `false`.
1847+
* `filter` {Function} Function to filter copied files/directories. Return
1848+
`true` to copy the item, `false` to ignore it. Can also return a `Promise`
1849+
that resolves to `true` or `false` **Default:** `undefined`.
1850+
* `force` {boolean} overwrite existing file or directory. _The copy
1851+
operation will ignore errors if you set this to false and the destination
1852+
exists. Use the `errorOnExist` option to change this behavior.
1853+
**Default:** `true`.
1854+
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
1855+
be preserved. **Default:** `false`.
1856+
* `recursive` {boolean} copy directories recursively **Default:** `false`
1857+
* `callback` {Function}
1858+
1859+
Asynchronously copies the entire directory structure from `src` to `dest`,
1860+
including subdirectories and files.
1861+
1862+
When copying a directory to another directory, globs are not supported and
1863+
behavior is similar to `cp dir1/ dir2/`.
1864+
18031865
### `fs.createReadStream(path[, options])`
18041866
<!-- YAML
18051867
added: v0.1.31
@@ -4267,6 +4329,35 @@ console.log('source.txt was copied to destination.txt');
42674329
copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL);
42684330
```
42694331
4332+
### `fs.cpSync(src, dest[, options])`
4333+
<!-- YAML
4334+
added: REPLACEME
4335+
-->
4336+
4337+
> Stability: 1 - Experimental
4338+
4339+
* `src` {string|URL} source path to copy.
4340+
* `dest` {string|URL} destination path to copy to.
4341+
* `options` {Object}
4342+
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
4343+
* `errorOnExist` {boolean} when `force` is `false`, and the destination
4344+
exists, throw an error. **Default:** `false`.
4345+
* `filter` {Function} Function to filter copied files/directories. Return
4346+
`true` to copy the item, `false` to ignore it. **Default:** `undefined`
4347+
* `force` {boolean} overwrite existing file or directory. _The copy
4348+
operation will ignore errors if you set this to false and the destination
4349+
exists. Use the `errorOnExist` option to change this behavior.
4350+
**Default:** `true`.
4351+
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
4352+
be preserved. **Default:** `false`.
4353+
* `recursive` {boolean} copy directories recursively **Default:** `false`
4354+
4355+
Synchronously copies the entire directory structure from `src` to `dest`,
4356+
including subdirectories and files.
4357+
4358+
When copying a directory to another directory, globs are not supported and
4359+
behavior is similar to `cp dir1/ dir2/`.
4360+
42704361
### `fs.existsSync(path)`
42714362
<!-- YAML
42724363
added: v0.1.21

lib/fs.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ const {
107107
stringToSymlinkType,
108108
toUnixTimestamp,
109109
validateBufferArray,
110+
validateCpOptions,
110111
validateOffsetLengthRead,
111112
validateOffsetLengthWrite,
112113
validatePath,
@@ -145,6 +146,8 @@ let truncateWarn = true;
145146
let fs;
146147

147148
// Lazy loaded
149+
let cpFn;
150+
let cpSyncFn;
148151
let promises = null;
149152
let ReadStream;
150153
let WriteStream;
@@ -1075,6 +1078,13 @@ function ftruncateSync(fd, len = 0) {
10751078
handleErrorFromBinding(ctx);
10761079
}
10771080

1081+
function lazyLoadCp() {
1082+
if (cpFn === undefined) {
1083+
({ cpFn } = require('internal/fs/cp/cp'));
1084+
cpFn = require('util').callbackify(cpFn);
1085+
({ cpSyncFn } = require('internal/fs/cp/cp-sync'));
1086+
}
1087+
}
10781088

10791089
function lazyLoadRimraf() {
10801090
if (rimraf === undefined)
@@ -2790,6 +2800,44 @@ function copyFileSync(src, dest, mode) {
27902800
handleErrorFromBinding(ctx);
27912801
}
27922802

2803+
/**
2804+
* Asynchronously copies `src` to `dest`. `src` can be a file, directory, or
2805+
* symlink. The contents of directories will be copied recursively.
2806+
* @param {string | URL} src
2807+
* @param {string | URL} dest
2808+
* @param {Object} [options]
2809+
* @param {() => any} callback
2810+
* @returns {void}
2811+
*/
2812+
function cp(src, dest, options, callback) {
2813+
if (typeof options === 'function') {
2814+
callback = options;
2815+
options = undefined;
2816+
}
2817+
callback = makeCallback(callback);
2818+
options = validateCpOptions(options);
2819+
src = pathModule.toNamespacedPath(getValidatedPath(src, 'src'));
2820+
dest = pathModule.toNamespacedPath(getValidatedPath(dest, 'dest'));
2821+
lazyLoadCp();
2822+
cpFn(src, dest, options, callback);
2823+
}
2824+
2825+
/**
2826+
* Synchronously copies `src` to `dest`. `src` can be a file, directory, or
2827+
* symlink. The contents of directories will be copied recursively.
2828+
* @param {string | URL} src
2829+
* @param {string | URL} dest
2830+
* @param {Object} [options]
2831+
* @returns {void}
2832+
*/
2833+
function cpSync(src, dest, options) {
2834+
options = validateCpOptions(options);
2835+
src = pathModule.toNamespacedPath(getValidatedPath(src, 'src'));
2836+
dest = pathModule.toNamespacedPath(getValidatedPath(dest, 'dest'));
2837+
lazyLoadCp();
2838+
cpSyncFn(src, dest, options);
2839+
}
2840+
27932841
function lazyLoadStreams() {
27942842
if (!ReadStream) {
27952843
({ ReadStream, WriteStream } = require('internal/fs/streams'));
@@ -2854,6 +2902,8 @@ module.exports = fs = {
28542902
closeSync,
28552903
copyFile,
28562904
copyFileSync,
2905+
cp,
2906+
cpSync,
28572907
createReadStream,
28582908
createWriteStream,
28592909
exists,

lib/internal/errors.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,17 @@ E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
961961
'The feature %s is unavailable on the current platform' +
962962
', which is being used to run Node.js',
963963
TypeError);
964+
E('ERR_FS_CP_DIR_TO_NON_DIR',
965+
'Cannot overwrite directory with non-directory', SystemError);
966+
E('ERR_FS_CP_EEXIST', 'Target already exists', SystemError);
967+
E('ERR_FS_CP_EINVAL', 'Invalid src or dest', SystemError);
968+
E('ERR_FS_CP_FIFO_PIPE', 'Cannot copy a FIFO pipe', SystemError);
969+
E('ERR_FS_CP_NON_DIR_TO_DIR',
970+
'Cannot overwrite non-directory with directory', SystemError);
971+
E('ERR_FS_CP_SOCKET', 'Cannot copy a socket file', SystemError);
972+
E('ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY',
973+
'Cannot overwrite symlink in subdirectory of self', SystemError);
974+
E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type', SystemError);
964975
E('ERR_FS_EISDIR', 'Path is a directory', SystemError);
965976
E('ERR_FS_FILE_TOO_LARGE', 'File size (%s) is greater than 2 GB', RangeError);
966977
E('ERR_FS_INVALID_SYMLINK_TYPE',

0 commit comments

Comments
 (0)