Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs: support copy of relative links with cp and cpSync #41819

Merged
merged 12 commits into from Feb 15, 2022
20 changes: 20 additions & 0 deletions doc/api/fs.md
Expand Up @@ -876,6 +876,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 @@ -896,6 +901,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 @@ -2094,6 +2101,10 @@ 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.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/41678
description: Passing an invalid callback to the `callback` argument
Expand All @@ -2119,6 +2130,8 @@ changes:
* `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 @@ -4856,6 +4869,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 @@ -4875,6 +4893,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');
marcosbc marked this conversation as resolved.
Show resolved Hide resolved
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