Skip to content

Commit

Permalink
fs: add recursive cp method
Browse files Browse the repository at this point in the history
Introduces recursive cp method, based on fs-extra implementation.

PR-URL: #39372
Fixes: #35880
Refs: nodejs/tooling#98
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ian Sutherland <ian@iansutherland.ca>
  • Loading branch information
bcoe committed Aug 12, 2021
1 parent 4ece669 commit 87d6fd7
Show file tree
Hide file tree
Showing 20 changed files with 1,782 additions and 1 deletion.
19 changes: 19 additions & 0 deletions LICENSE
Expand Up @@ -1584,3 +1584,22 @@ The externally maintained libraries used by Node.js are:
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

- node-fs-extra, located at lib/internal/fs/cp, is licensed as follows:
"""
(The MIT License)

Copyright (c) 2011-2017 JP Richardson

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
69 changes: 69 additions & 0 deletions doc/api/errors.md
Expand Up @@ -1115,6 +1115,74 @@ added: v14.0.0
Used when a feature that is not available
to the current platform which is running Node.js is used.

<a id="ERR_FS_CP_DIR_TO_NON_DIR"></a>
### `ERR_FS_CP_DIR_TO_NON_DIR`
<!--
added: REPLACEME
-->

An attempt was made to copy a directory to a non-directory (file, symlink,
etc.) using [`fs.cp()`][].

<a id="ERR_FS_CP_EEXIST"></a>
### `ERR_FS_CP_EEXIST`
<!--
added: REPLACEME
-->

An attempt was made to copy over a file that already existed with
[`fs.cp()`][], with the `force` and `errorOnExist` set to `true`.

<a id="ERR_FS_CP_EINVAL"></a>
### `ERR_FS_CP_EINVAL`
<!--
added: REPLACEME
-->

When using [`fs.cp()`][], `src` or `dest` pointed to an invalid path.

<a id="ERR_FS_CP_FIFO_PIPE"></a>
### `ERR_FS_CP_FIFO_PIPE`
<!--
added: REPLACEME
-->

An attempt was made to copy a named pipe with [`fs.cp()`][].

<a id="ERR_FS_CP_NON_DIR_TO_DIR"></a>
### `ERR_FS_CP_NON_DIR_TO_DIR`
<!--
added: REPLACEME
-->

An attempt was made to copy a non-directory (file, symlink, etc.) to a directory
using [`fs.cp()`][].

<a id="ERR_FS_CP_SOCKET"></a>
### `ERR_FS_CP_SOCKET`
<!--
added: REPLACEME
-->

An attempt was made to copy to a socket with [`fs.cp()`][].

<a id="ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY"></a>
### `ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY`
<!--
added: REPLACEME
-->

When using [`fs.cp()`][], a symlink in `dest` pointed to a subdirectory
of `src`.

<a id="ERR_FS_CP_UNKNOWN"></a>
### `ERR_FS_CP_UNKNOWN`
<!--
added: REPLACEME
-->

An attempt was made to copy to an unknown file type with [`fs.cp()`][].

<a id="ERR_FS_EISDIR"></a>
### `ERR_FS_EISDIR`

Expand Down Expand Up @@ -2822,6 +2890,7 @@ The native call from `process.cpuUsage` could not be processed.
[`dgram.remoteAddress()`]: dgram.md#dgram_socket_remoteaddress
[`errno`(3) man page]: https://man7.org/linux/man-pages/man3/errno.3.html
[`fs.Dir`]: fs.md#fs_class_fs_dir
[`fs.cp()`]: fs.md#fs_fs_cp_src_dest_options_callback
[`fs.readFileSync`]: fs.md#fs_fs_readfilesync_path_options
[`fs.readdir`]: fs.md#fs_fs_readdir_path_options_callback
[`fs.symlink()`]: fs.md#fs_fs_symlink_target_path_type_callback
Expand Down
91 changes: 91 additions & 0 deletions doc/api/fs.md
Expand Up @@ -739,6 +739,37 @@ try {
}
```
### `fsPromises.cp(src, dest[, options])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `src` {string|URL} source path to copy.
* `dest` {string|URL} destination path to copy to.
* `options` {Object}
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
* `errorOnExist` {boolean} when `force` is `false`, and the destination
exists, throw an error. **Default:** `false`.
* `filter` {Function} Function to filter copied files/directories. Return
`true` to copy the item, `false` to ignore it. Can also return a `Promise`
that resolves to `true` or `false` **Default:** `undefined`.
* `force` {boolean} overwrite existing file or directory. _The copy
operation will ignore errors if you set this to false and the destination
exists. Use the `errorOnExist` option to change this behavior.
**Default:** `true`.
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
be preserved. **Default:** `false`.
* `recursive` {boolean} copy directories recursively **Default:** `false`
* Returns: {Promise} Fulfills with `undefined` upon success.
Asynchronously copies the entire directory structure from `src` to `dest`,
including subdirectories and files.
When copying a directory to another directory, globs are not supported and
behavior is similar to `cp dir1/ dir2/`.
### `fsPromises.lchmod(path, mode)`
<!-- YAML
deprecated: v10.0.0
Expand Down Expand Up @@ -1848,6 +1879,37 @@ copyFile('source.txt', 'destination.txt', callback);
copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback);
```
### `fs.cp(src, dest[, options], callback)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `src` {string|URL} source path to copy.
* `dest` {string|URL} destination path to copy to.
* `options` {Object}
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
* `errorOnExist` {boolean} when `force` is `false`, and the destination
exists, throw an error. **Default:** `false`.
* `filter` {Function} Function to filter copied files/directories. Return
`true` to copy the item, `false` to ignore it. Can also return a `Promise`
that resolves to `true` or `false` **Default:** `undefined`.
* `force` {boolean} overwrite existing file or directory. _The copy
operation will ignore errors if you set this to false and the destination
exists. Use the `errorOnExist` option to change this behavior.
**Default:** `true`.
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
be preserved. **Default:** `false`.
* `recursive` {boolean} copy directories recursively **Default:** `false`
* `callback` {Function}
Asynchronously copies the entire directory structure from `src` to `dest`,
including subdirectories and files.
When copying a directory to another directory, globs are not supported and
behavior is similar to `cp dir1/ dir2/`.
### `fs.createReadStream(path[, options])`
<!-- YAML
added: v0.1.31
Expand Down Expand Up @@ -4321,6 +4383,35 @@ console.log('source.txt was copied to destination.txt');
copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL);
```
### `fs.cpSync(src, dest[, options])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* `src` {string|URL} source path to copy.
* `dest` {string|URL} destination path to copy to.
* `options` {Object}
* `dereference` {boolean} dereference symlinks. **Default:** `false`.
* `errorOnExist` {boolean} when `force` is `false`, and the destination
exists, throw an error. **Default:** `false`.
* `filter` {Function} Function to filter copied files/directories. Return
`true` to copy the item, `false` to ignore it. **Default:** `undefined`
* `force` {boolean} overwrite existing file or directory. _The copy
operation will ignore errors if you set this to false and the destination
exists. Use the `errorOnExist` option to change this behavior.
**Default:** `true`.
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
be preserved. **Default:** `false`.
* `recursive` {boolean} copy directories recursively **Default:** `false`
Synchronously copies the entire directory structure from `src` to `dest`,
including subdirectories and files.
When copying a directory to another directory, globs are not supported and
behavior is similar to `cp dir1/ dir2/`.
### `fs.existsSync(path)`
<!-- YAML
added: v0.1.21
Expand Down
50 changes: 50 additions & 0 deletions lib/fs.js
Expand Up @@ -107,6 +107,7 @@ const {
stringToSymlinkType,
toUnixTimestamp,
validateBufferArray,
validateCpOptions,
validateOffsetLengthRead,
validateOffsetLengthWrite,
validatePath,
Expand Down Expand Up @@ -145,6 +146,8 @@ let truncateWarn = true;
let fs;

// Lazy loaded
let cpFn;
let cpSyncFn;
let promises = null;
let ReadStream;
let WriteStream;
Expand Down Expand Up @@ -1075,6 +1078,13 @@ function ftruncateSync(fd, len = 0) {
handleErrorFromBinding(ctx);
}

function lazyLoadCp() {
if (cpFn === undefined) {
({ cpFn } = require('internal/fs/cp/cp'));
cpFn = require('util').callbackify(cpFn);
({ cpSyncFn } = require('internal/fs/cp/cp-sync'));
}
}

function lazyLoadRimraf() {
if (rimraf === undefined)
Expand Down Expand Up @@ -2790,6 +2800,44 @@ function copyFileSync(src, dest, mode) {
handleErrorFromBinding(ctx);
}

/**
* Asynchronously copies `src` to `dest`. `src` can be a file, directory, or
* symlink. The contents of directories will be copied recursively.
* @param {string | URL} src
* @param {string | URL} dest
* @param {Object} [options]
* @param {() => any} callback
* @returns {void}
*/
function cp(src, dest, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
callback = makeCallback(callback);
options = validateCpOptions(options);
src = pathModule.toNamespacedPath(getValidatedPath(src, 'src'));
dest = pathModule.toNamespacedPath(getValidatedPath(dest, 'dest'));
lazyLoadCp();
cpFn(src, dest, options, callback);
}

/**
* Synchronously copies `src` to `dest`. `src` can be a file, directory, or
* symlink. The contents of directories will be copied recursively.
* @param {string | URL} src
* @param {string | URL} dest
* @param {Object} [options]
* @returns {void}
*/
function cpSync(src, dest, options) {
options = validateCpOptions(options);
src = pathModule.toNamespacedPath(getValidatedPath(src, 'src'));
dest = pathModule.toNamespacedPath(getValidatedPath(dest, 'dest'));
lazyLoadCp();
cpSyncFn(src, dest, options);
}

function lazyLoadStreams() {
if (!ReadStream) {
({ ReadStream, WriteStream } = require('internal/fs/streams'));
Expand Down Expand Up @@ -2854,6 +2902,8 @@ module.exports = fs = {
closeSync,
copyFile,
copyFileSync,
cp,
cpSync,
createReadStream,
createWriteStream,
exists,
Expand Down
11 changes: 11 additions & 0 deletions lib/internal/errors.js
Expand Up @@ -961,6 +961,17 @@ E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM',
'The feature %s is unavailable on the current platform' +
', which is being used to run Node.js',
TypeError);
E('ERR_FS_CP_DIR_TO_NON_DIR',
'Cannot overwrite directory with non-directory', SystemError);
E('ERR_FS_CP_EEXIST', 'Target already exists', SystemError);
E('ERR_FS_CP_EINVAL', 'Invalid src or dest', SystemError);
E('ERR_FS_CP_FIFO_PIPE', 'Cannot copy a FIFO pipe', SystemError);
E('ERR_FS_CP_NON_DIR_TO_DIR',
'Cannot overwrite non-directory with directory', SystemError);
E('ERR_FS_CP_SOCKET', 'Cannot copy a socket file', SystemError);
E('ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY',
'Cannot overwrite symlink in subdirectory of self', SystemError);
E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type', SystemError);
E('ERR_FS_EISDIR', 'Path is a directory', SystemError);
E('ERR_FS_FILE_TOO_LARGE', 'File size (%s) is greater than 2 GB', RangeError);
E('ERR_FS_INVALID_SYMLINK_TYPE',
Expand Down

0 comments on commit 87d6fd7

Please sign in to comment.