Skip to content

Commit 87d6fd7

Browse files
committed
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 4ece669 commit 87d6fd7

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
@@ -1115,6 +1115,74 @@ added: v14.0.0
11151115
Used when a feature that is not available
11161116
to the current platform which is running Node.js is used.
11171117

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

@@ -2822,6 +2890,7 @@ The native call from `process.cpuUsage` could not be processed.
28222890
[`dgram.remoteAddress()`]: dgram.md#dgram_socket_remoteaddress
28232891
[`errno`(3) man page]: https://man7.org/linux/man-pages/man3/errno.3.html
28242892
[`fs.Dir`]: fs.md#fs_class_fs_dir
2893+
[`fs.cp()`]: fs.md#fs_fs_cp_src_dest_options_callback
28252894
[`fs.readFileSync`]: fs.md#fs_fs_readfilesync_path_options
28262895
[`fs.readdir`]: fs.md#fs_fs_readdir_path_options_callback
28272896
[`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
@@ -739,6 +739,37 @@ try {
739739
}
740740
```
741741
742+
### `fsPromises.cp(src, dest[, options])`
743+
<!-- YAML
744+
added: REPLACEME
745+
-->
746+
747+
> Stability: 1 - Experimental
748+
749+
* `src` {string|URL} source path to copy.
750+
* `dest` {string|URL} destination path to copy to.
751+
* `options` {Object}
752+
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
753+
* `errorOnExist` {boolean} when `force` is `false`, and the destination
754+
exists, throw an error. **Default:** `false`.
755+
* `filter` {Function} Function to filter copied files/directories. Return
756+
`true` to copy the item, `false` to ignore it. Can also return a `Promise`
757+
that resolves to `true` or `false` **Default:** `undefined`.
758+
* `force` {boolean} overwrite existing file or directory. _The copy
759+
operation will ignore errors if you set this to false and the destination
760+
exists. Use the `errorOnExist` option to change this behavior.
761+
**Default:** `true`.
762+
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
763+
be preserved. **Default:** `false`.
764+
* `recursive` {boolean} copy directories recursively **Default:** `false`
765+
* Returns: {Promise} Fulfills with `undefined` upon success.
766+
767+
Asynchronously copies the entire directory structure from `src` to `dest`,
768+
including subdirectories and files.
769+
770+
When copying a directory to another directory, globs are not supported and
771+
behavior is similar to `cp dir1/ dir2/`.
772+
742773
### `fsPromises.lchmod(path, mode)`
743774
<!-- YAML
744775
deprecated: v10.0.0
@@ -1848,6 +1879,37 @@ copyFile('source.txt', 'destination.txt', callback);
18481879
copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback);
18491880
```
18501881
1882+
### `fs.cp(src, dest[, options], callback)`
1883+
<!-- YAML
1884+
added: REPLACEME
1885+
-->
1886+
1887+
> Stability: 1 - Experimental
1888+
1889+
* `src` {string|URL} source path to copy.
1890+
* `dest` {string|URL} destination path to copy to.
1891+
* `options` {Object}
1892+
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
1893+
* `errorOnExist` {boolean} when `force` is `false`, and the destination
1894+
exists, throw an error. **Default:** `false`.
1895+
* `filter` {Function} Function to filter copied files/directories. Return
1896+
`true` to copy the item, `false` to ignore it. Can also return a `Promise`
1897+
that resolves to `true` or `false` **Default:** `undefined`.
1898+
* `force` {boolean} overwrite existing file or directory. _The copy
1899+
operation will ignore errors if you set this to false and the destination
1900+
exists. Use the `errorOnExist` option to change this behavior.
1901+
**Default:** `true`.
1902+
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
1903+
be preserved. **Default:** `false`.
1904+
* `recursive` {boolean} copy directories recursively **Default:** `false`
1905+
* `callback` {Function}
1906+
1907+
Asynchronously copies the entire directory structure from `src` to `dest`,
1908+
including subdirectories and files.
1909+
1910+
When copying a directory to another directory, globs are not supported and
1911+
behavior is similar to `cp dir1/ dir2/`.
1912+
18511913
### `fs.createReadStream(path[, options])`
18521914
<!-- YAML
18531915
added: v0.1.31
@@ -4321,6 +4383,35 @@ console.log('source.txt was copied to destination.txt');
43214383
copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL);
43224384
```
43234385
4386+
### `fs.cpSync(src, dest[, options])`
4387+
<!-- YAML
4388+
added: REPLACEME
4389+
-->
4390+
4391+
> Stability: 1 - Experimental
4392+
4393+
* `src` {string|URL} source path to copy.
4394+
* `dest` {string|URL} destination path to copy to.
4395+
* `options` {Object}
4396+
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
4397+
* `errorOnExist` {boolean} when `force` is `false`, and the destination
4398+
exists, throw an error. **Default:** `false`.
4399+
* `filter` {Function} Function to filter copied files/directories. Return
4400+
`true` to copy the item, `false` to ignore it. **Default:** `undefined`
4401+
* `force` {boolean} overwrite existing file or directory. _The copy
4402+
operation will ignore errors if you set this to false and the destination
4403+
exists. Use the `errorOnExist` option to change this behavior.
4404+
**Default:** `true`.
4405+
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
4406+
be preserved. **Default:** `false`.
4407+
* `recursive` {boolean} copy directories recursively **Default:** `false`
4408+
4409+
Synchronously copies the entire directory structure from `src` to `dest`,
4410+
including subdirectories and files.
4411+
4412+
When copying a directory to another directory, globs are not supported and
4413+
behavior is similar to `cp dir1/ dir2/`.
4414+
43244415
### `fs.existsSync(path)`
43254416
<!-- YAML
43264417
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)