Skip to content

Commit

Permalink
fs: support copy of relative links with cp and cpSync
Browse files Browse the repository at this point in the history
Fixes: #41693

PR-URL: #41819
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
  • Loading branch information
marcosbc authored and danielleadams committed Apr 24, 2022
1 parent 8858950 commit 33e4a32
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 6 deletions.
21 changes: 21 additions & 0 deletions doc/api/fs.md
Expand Up @@ -827,6 +827,11 @@ try {
<!-- YAML
added: v16.7.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41819
description: Accepts an additional `verbatimSymlinks` option to specify
whether to perform path resolution for symlinks.
-->
> Stability: 1 - Experimental
Expand All @@ -847,6 +852,8 @@ added: v16.7.0
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
be preserved. **Default:** `false`.
* `recursive` {boolean} copy directories recursively **Default:** `false`
* `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will
be skipped. **Default:** `false`
* Returns: {Promise} Fulfills with `undefined` upon success.
Asynchronously copies the entire directory structure from `src` to `dest`,
Expand Down Expand Up @@ -2006,6 +2013,11 @@ copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback);
<!-- YAML
added: v16.7.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41819
description: Accepts an additional `verbatimSymlinks` option to specify
whether to perform path resolution for symlinks.
-->
> Stability: 1 - Experimental
Expand All @@ -2026,6 +2038,8 @@ added: v16.7.0
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
be preserved. **Default:** `false`.
* `recursive` {boolean} copy directories recursively **Default:** `false`
* `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will
be skipped. **Default:** `false`
* `callback` {Function}
Asynchronously copies the entire directory structure from `src` to `dest`,
Expand Down Expand Up @@ -4581,6 +4595,11 @@ copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL);
<!-- YAML
added: v16.7.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41819
description: Accepts an additional `verbatimSymlinks` option to specify
whether to perform path resolution for symlinks.
-->
> Stability: 1 - Experimental
Expand All @@ -4600,6 +4619,8 @@ added: v16.7.0
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
be preserved. **Default:** `false`.
* `recursive` {boolean} copy directories recursively **Default:** `false`
* `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will
be skipped. **Default:** `false`
Synchronously copies the entire directory structure from `src` to `dest`,
including subdirectories and files.
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/fs/cp/cp-sync.js
Expand Up @@ -182,7 +182,7 @@ function getStats(destStat, src, dest, opts) {
srcStat.isBlockDevice()) {
return onFile(srcStat, destStat, src, dest, opts);
} else if (srcStat.isSymbolicLink()) {
return onLink(destStat, src, dest);
return onLink(destStat, src, dest, opts);
} else if (srcStat.isSocket()) {
throw new ERR_FS_CP_SOCKET({
message: `cannot copy a socket file: ${dest}`,
Expand Down Expand Up @@ -293,9 +293,9 @@ function copyDir(src, dest, opts) {
}
}

function onLink(destStat, src, dest) {
function onLink(destStat, src, dest, opts) {
let resolvedSrc = readlinkSync(src);
if (!isAbsolute(resolvedSrc)) {
if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) {
resolvedSrc = resolve(dirname(src), resolvedSrc);
}
if (!destStat) {
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/fs/cp/cp.js
Expand Up @@ -222,7 +222,7 @@ async function getStatsForCopy(destStat, src, dest, opts) {
srcStat.isBlockDevice()) {
return onFile(srcStat, destStat, src, dest, opts);
} else if (srcStat.isSymbolicLink()) {
return onLink(destStat, src, dest);
return onLink(destStat, src, dest, opts);
} else if (srcStat.isSocket()) {
throw new ERR_FS_CP_SOCKET({
message: `cannot copy a socket file: ${dest}`,
Expand Down Expand Up @@ -335,9 +335,9 @@ async function copyDir(src, dest, opts) {
}
}

async function onLink(destStat, src, dest) {
async function onLink(destStat, src, dest, opts) {
let resolvedSrc = await readlink(src);
if (!isAbsolute(resolvedSrc)) {
if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) {
resolvedSrc = resolve(dirname(src), resolvedSrc);
}
if (!destStat) {
Expand Down
6 changes: 6 additions & 0 deletions lib/internal/fs/utils.js
Expand Up @@ -29,6 +29,7 @@ const {
codes: {
ERR_FS_EISDIR,
ERR_FS_INVALID_SYMLINK_TYPE,
ERR_INCOMPATIBLE_OPTION_PAIR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_OUT_OF_RANGE
Expand Down Expand Up @@ -724,6 +725,7 @@ const defaultCpOptions = {
force: true,
preserveTimestamps: false,
recursive: false,
verbatimSymlinks: false,
};

const defaultRmOptions = {
Expand All @@ -749,6 +751,10 @@ const validateCpOptions = hideStackFrames((options) => {
validateBoolean(options.force, 'options.force');
validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps');
validateBoolean(options.recursive, 'options.recursive');
validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks');
if (options.dereference === true && options.verbatimSymlinks === true) {
throw new ERR_INCOMPATIBLE_OPTION_PAIR('dereference', 'verbatimSymlinks');
}
if (options.filter !== undefined) {
validateFunction(options.filter, 'options.filter');
}
Expand Down
71 changes: 71 additions & 0 deletions test/parallel/test-fs-cp.mjs
Expand Up @@ -95,6 +95,77 @@ function nextdir() {
}


// It throws error when verbatimSymlinks is not a boolean.
{
const src = './test/fixtures/copy/kitchen-sink';
[1, [], {}, null, 1n, undefined, null, Symbol(), '', () => {}]
.forEach((verbatimSymlinks) => {
assert.throws(
() => cpSync(src, src, { verbatimSymlinks }),
{ code: 'ERR_INVALID_ARG_TYPE' }
);
});
}


// It throws an error when both dereference and verbatimSymlinks are enabled.
{
const src = './test/fixtures/copy/kitchen-sink';
assert.throws(
() => cpSync(src, src, { dereference: true, verbatimSymlinks: true }),
{ code: 'ERR_INCOMPATIBLE_OPTION_PAIR' }
);
}


// It resolves relative symlinks to their absolute path by default.
{
const src = nextdir();
mkdirSync(src, { recursive: true });
writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
symlinkSync('foo.js', join(src, 'bar.js'));

const dest = nextdir();
mkdirSync(dest, { recursive: true });

cpSync(src, dest, { recursive: true });
const link = readlinkSync(join(dest, 'bar.js'));
assert.strictEqual(link, join(src, 'foo.js'));
}


// It resolves relative symlinks when verbatimSymlinks is false.
{
const src = nextdir();
mkdirSync(src, { recursive: true });
writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
symlinkSync('foo.js', join(src, 'bar.js'));

const dest = nextdir();
mkdirSync(dest, { recursive: true });

cpSync(src, dest, { recursive: true, verbatimSymlinks: false });
const link = readlinkSync(join(dest, 'bar.js'));
assert.strictEqual(link, join(src, 'foo.js'));
}


// It does not resolve relative symlinks when verbatimSymlinks is true.
{
const src = nextdir();
mkdirSync(src, { recursive: true });
writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
symlinkSync('foo.js', join(src, 'bar.js'));

const dest = nextdir();
mkdirSync(dest, { recursive: true });

cpSync(src, dest, { recursive: true, verbatimSymlinks: true });
const link = readlinkSync(join(dest, 'bar.js'));
assert.strictEqual(link, 'foo.js');
}


// It throws error when src and dest are identical.
{
const src = './test/fixtures/copy/kitchen-sink';
Expand Down

0 comments on commit 33e4a32

Please sign in to comment.