Skip to content

Commit 2325ccb

Browse files
committed
fs: support copy of relative links with cp and cpSync
Fixes: #41693
1 parent 1387bb5 commit 2325ccb

File tree

5 files changed

+47
-6
lines changed

5 files changed

+47
-6
lines changed

doc/api/fs.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,8 @@ added: v16.7.0
896896
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
897897
be preserved. **Default:** `false`.
898898
* `recursive` {boolean} copy directories recursively **Default:** `false`
899+
* `absolute` {boolean} convert relative symlinks to absolute symlinks.
900+
**Default:** `true`
899901
* Returns: {Promise} Fulfills with `undefined` upon success.
900902
901903
Asynchronously copies the entire directory structure from `src` to `dest`,
@@ -2083,6 +2085,8 @@ added: v16.7.0
20832085
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
20842086
be preserved. **Default:** `false`.
20852087
* `recursive` {boolean} copy directories recursively **Default:** `false`
2088+
* `absolute` {boolean} convert relative symlinks to absolute symlinks.
2089+
**Default:** `true`
20862090
* `callback` {Function}
20872091
20882092
Asynchronously copies the entire directory structure from `src` to `dest`,
@@ -4665,6 +4669,8 @@ added: v16.7.0
46654669
* `preserveTimestamps` {boolean} When `true` timestamps from `src` will
46664670
be preserved. **Default:** `false`.
46674671
* `recursive` {boolean} copy directories recursively **Default:** `false`
4672+
* `absolute` {boolean} convert relative symlinks to absolute symlinks.
4673+
**Default:** `true`
46684674
46694675
Synchronously copies the entire directory structure from `src` to `dest`,
46704676
including subdirectories and files.

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

Lines changed: 3 additions & 3 deletions
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.absolute && !isAbsolute(resolvedSrc)) {
299299
resolvedSrc = resolve(dirname(src), resolvedSrc);
300300
}
301301
if (!destStat) {

lib/internal/fs/cp/cp.js

Lines changed: 3 additions & 3 deletions
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.absolute && !isAbsolute(resolvedSrc)) {
341341
resolvedSrc = resolve(dirname(src), resolvedSrc);
342342
}
343343
if (!destStat) {

lib/internal/fs/utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ const defaultCpOptions = {
724724
force: true,
725725
preserveTimestamps: false,
726726
recursive: false,
727+
absolute: true,
727728
};
728729

729730
const defaultRmOptions = {
@@ -749,6 +750,7 @@ const validateCpOptions = hideStackFrames((options) => {
749750
validateBoolean(options.force, 'options.force');
750751
validateBoolean(options.preserveTimestamps, 'options.preserveTimestamps');
751752
validateBoolean(options.recursive, 'options.recursive');
753+
validateBoolean(options.absolute, 'options.absolute');
752754
if (options.filter !== undefined) {
753755
validateFunction(options.filter, 'options.filter');
754756
}

test/parallel/test-fs-cp.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,39 @@ function nextdir() {
9595
}
9696

9797

98+
// It resolves relative symlinks to absolute path when absolute is true
99+
{
100+
const src = nextdir();
101+
mkdirSync(src, { recursive: true });
102+
writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
103+
symlinkSync('foo.js', join(src, 'bar.js'));
104+
105+
const dest = nextdir();
106+
mkdirSync(dest, { recursive: true });
107+
108+
cpSync(src, dest, { recursive: true, absolute: true });
109+
const link = readlinkSync(join(dest, 'bar.js'));
110+
assert.strictEqual(link, join(src, 'foo.js'));
111+
}
112+
113+
114+
// It copies relative symlink when absolute is false
115+
{
116+
const src = nextdir();
117+
mkdirSync(src, { recursive: true });
118+
writeFileSync(join(src, 'foo.js'), 'foo', 'utf8');
119+
symlinkSync('foo.js', join(src, 'bar.js'));
120+
121+
const dest = nextdir();
122+
mkdirSync(dest, { recursive: true });
123+
const destFile = join(dest, 'foo.js');
124+
125+
cpSync(src, dest, { recursive: true, absolute: false });
126+
const link = readlinkSync(join(dest, 'bar.js'));
127+
assert.strictEqual(link, 'foo.js');
128+
}
129+
130+
98131
// It throws error when src and dest are identical.
99132
{
100133
const src = './test/fixtures/copy/kitchen-sink';

0 commit comments

Comments
 (0)