Skip to content

Commit 1e862b3

Browse files
marcosbcbengl
authored andcommitted
fs: support copy of relative links with cp and cpSync
Fixes: #41693 PR-URL: #41819 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Tobias Nießen <[email protected]>
1 parent 326f545 commit 1e862b3

File tree

5 files changed

+104
-6
lines changed

5 files changed

+104
-6
lines changed

doc/api/fs.md

+21
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,11 @@ try {
876876
877877
<!-- YAML
878878
added: v16.7.0
879+
changes:
880+
- version: REPLACEME
881+
pr-url: https://github.com/nodejs/node/pull/41819
882+
description: Accepts an additional `verbatimSymlinks` option to specify
883+
whether to perform path resolution for symlinks.
879884
-->
880885
881886
> Stability: 1 - Experimental
@@ -896,6 +901,8 @@ added: v16.7.0
896901
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
897902
be preserved. **Default:** `false`.
898903
* `recursive` {boolean} copy directories recursively **Default:** `false`
904+
* `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will
905+
be skipped. **Default:** `false`
899906
* Returns: {Promise} Fulfills with `undefined` upon success.
900907
901908
Asynchronously copies the entire directory structure from `src` to `dest`,
@@ -2063,6 +2070,11 @@ copyFile('source.txt', 'destination.txt', constants.COPYFILE_EXCL, callback);
20632070
20642071
<!-- YAML
20652072
added: v16.7.0
2073+
changes:
2074+
- version: REPLACEME
2075+
pr-url: https://github.com/nodejs/node/pull/41819
2076+
description: Accepts an additional `verbatimSymlinks` option to specify
2077+
whether to perform path resolution for symlinks.
20662078
-->
20672079
20682080
> Stability: 1 - Experimental
@@ -2083,6 +2095,8 @@ added: v16.7.0
20832095
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
20842096
be preserved. **Default:** `false`.
20852097
* `recursive` {boolean} copy directories recursively **Default:** `false`
2098+
* `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will
2099+
be skipped. **Default:** `false`
20862100
* `callback` {Function}
20872101
20882102
Asynchronously copies the entire directory structure from `src` to `dest`,
@@ -4646,6 +4660,11 @@ copyFileSync('source.txt', 'destination.txt', constants.COPYFILE_EXCL);
46464660
46474661
<!-- YAML
46484662
added: v16.7.0
4663+
changes:
4664+
- version: REPLACEME
4665+
pr-url: https://github.com/nodejs/node/pull/41819
4666+
description: Accepts an additional `verbatimSymlinks` option to specify
4667+
whether to perform path resolution for symlinks.
46494668
-->
46504669
46514670
> Stability: 1 - Experimental
@@ -4665,6 +4684,8 @@ added: v16.7.0
46654684
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
46664685
be preserved. **Default:** `false`.
46674686
* `recursive` {boolean} copy directories recursively **Default:** `false`
4687+
* `verbatimSymlinks` {boolean} When `true`, path resolution for symlinks will
4688+
be skipped. **Default:** `false`
46684689
46694690
Synchronously copies the entire directory structure from `src` to `dest`,
46704691
including subdirectories and files.

lib/internal/fs/cp/cp-sync.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ function getStats(destStat, src, dest, opts) {
182182
srcStat.isBlockDevice()) {
183183
return onFile(srcStat, destStat, src, dest, opts);
184184
} else if (srcStat.isSymbolicLink()) {
185-
return onLink(destStat, src, dest);
185+
return onLink(destStat, src, dest, opts);
186186
} else if (srcStat.isSocket()) {
187187
throw new ERR_FS_CP_SOCKET({
188188
message: `cannot copy a socket file: ${dest}`,
@@ -293,9 +293,9 @@ function copyDir(src, dest, opts) {
293293
}
294294
}
295295

296-
function onLink(destStat, src, dest) {
296+
function onLink(destStat, src, dest, opts) {
297297
let resolvedSrc = readlinkSync(src);
298-
if (!isAbsolute(resolvedSrc)) {
298+
if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) {
299299
resolvedSrc = resolve(dirname(src), resolvedSrc);
300300
}
301301
if (!destStat) {

lib/internal/fs/cp/cp.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ async function getStatsForCopy(destStat, src, dest, opts) {
222222
srcStat.isBlockDevice()) {
223223
return onFile(srcStat, destStat, src, dest, opts);
224224
} else if (srcStat.isSymbolicLink()) {
225-
return onLink(destStat, src, dest);
225+
return onLink(destStat, src, dest, opts);
226226
} else if (srcStat.isSocket()) {
227227
throw new ERR_FS_CP_SOCKET({
228228
message: `cannot copy a socket file: ${dest}`,
@@ -335,9 +335,9 @@ async function copyDir(src, dest, opts) {
335335
}
336336
}
337337

338-
async function onLink(destStat, src, dest) {
338+
async function onLink(destStat, src, dest, opts) {
339339
let resolvedSrc = await readlink(src);
340-
if (!isAbsolute(resolvedSrc)) {
340+
if (!opts.verbatimSymlinks && !isAbsolute(resolvedSrc)) {
341341
resolvedSrc = resolve(dirname(src), resolvedSrc);
342342
}
343343
if (!destStat) {

lib/internal/fs/utils.js

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const {
2929
codes: {
3030
ERR_FS_EISDIR,
3131
ERR_FS_INVALID_SYMLINK_TYPE,
32+
ERR_INCOMPATIBLE_OPTION_PAIR,
3233
ERR_INVALID_ARG_TYPE,
3334
ERR_INVALID_ARG_VALUE,
3435
ERR_OUT_OF_RANGE
@@ -724,6 +725,7 @@ const defaultCpOptions = {
724725
force: true,
725726
preserveTimestamps: false,
726727
recursive: false,
728+
verbatimSymlinks: false,
727729
};
728730

729731
const defaultRmOptions = {
@@ -749,6 +751,10 @@ const validateCpOptions = hideStackFrames((options) => {
749751
validateBoolean(options.force, 'options.force');
750752
validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps');
751753
validateBoolean(options.recursive, 'options.recursive');
754+
validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks');
755+
if (options.dereference === true && options.verbatimSymlinks === true) {
756+
throw new ERR_INCOMPATIBLE_OPTION_PAIR('dereference', 'verbatimSymlinks');
757+
}
752758
if (options.filter !== undefined) {
753759
validateFunction(options.filter, 'options.filter');
754760
}

test/parallel/test-fs-cp.mjs

+71
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,77 @@ function nextdir() {
9595
}
9696

9797

98+
// It throws error when verbatimSymlinks is not a boolean.
99+
{
100+
const src = './test/fixtures/copy/kitchen-sink';
101+
[1, [], {}, null, 1n, undefined, null, Symbol(), '', () => {}]
102+
.forEach((verbatimSymlinks) => {
103+
assert.throws(
104+
() => cpSync(src, src, { verbatimSymlinks }),
105+
{ code: 'ERR_INVALID_ARG_TYPE' }
106+
);
107+
});
108+
}
109+
110+
111+
// It throws an error when both dereference and verbatimSymlinks are enabled.
112+
{
113+
const src = './test/fixtures/copy/kitchen-sink';
114+
assert.throws(
115+
() => cpSync(src, src, { dereference: true, verbatimSymlinks: true }),
116+
{ code: 'ERR_INCOMPATIBLE_OPTION_PAIR' }
117+
);
118+
}
119+
120+
121+
// It resolves relative symlinks to their absolute path by default.
122+
{
123+
const src = nextdir();
124+
mkdirSync(src, { recursive: true });
125+
writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
126+
symlinkSync('foo.js', join(src, 'bar.js'));
127+
128+
const dest = nextdir();
129+
mkdirSync(dest, { recursive: true });
130+
131+
cpSync(src, dest, { recursive: true });
132+
const link = readlinkSync(join(dest, 'bar.js'));
133+
assert.strictEqual(link, join(src, 'foo.js'));
134+
}
135+
136+
137+
// It resolves relative symlinks when verbatimSymlinks is false.
138+
{
139+
const src = nextdir();
140+
mkdirSync(src, { recursive: true });
141+
writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
142+
symlinkSync('foo.js', join(src, 'bar.js'));
143+
144+
const dest = nextdir();
145+
mkdirSync(dest, { recursive: true });
146+
147+
cpSync(src, dest, { recursive: true, verbatimSymlinks: false });
148+
const link = readlinkSync(join(dest, 'bar.js'));
149+
assert.strictEqual(link, join(src, 'foo.js'));
150+
}
151+
152+
153+
// It does not resolve relative symlinks when verbatimSymlinks is true.
154+
{
155+
const src = nextdir();
156+
mkdirSync(src, { recursive: true });
157+
writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
158+
symlinkSync('foo.js', join(src, 'bar.js'));
159+
160+
const dest = nextdir();
161+
mkdirSync(dest, { recursive: true });
162+
163+
cpSync(src, dest, { recursive: true, verbatimSymlinks: true });
164+
const link = readlinkSync(join(dest, 'bar.js'));
165+
assert.strictEqual(link, 'foo.js');
166+
}
167+
168+
98169
// It throws error when src and dest are identical.
99170
{
100171
const src = './test/fixtures/copy/kitchen-sink';

0 commit comments

Comments
 (0)