Skip to content

Commit bb5fe4a

Browse files
committed
fs: add recursive copy method
Introduces recursive copy method, based on fs-extra implementation Refs: nodejs/tooling#98
1 parent d9f270b commit bb5fe4a

File tree

11 files changed

+89
-29
lines changed

11 files changed

+89
-29
lines changed

lib/internal/fs/copy/copy.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ function copyDirItem(items, item, src, dest, opts, cb) {
394394
function onLink(destStat, src, dest, opts, cb) {
395395
readlink(src, (err, resolvedSrc) => {
396396
if (err) return cb(err);
397+
// TODO(bcoe): I don't know how this could be called, because
398+
// getStatsForCopy will have used stat. Ask during review.
397399
if (opts.dereference) {
398400
resolvedSrc = resolve(process.cwd(), resolvedSrc);
399401
}
@@ -428,6 +430,8 @@ function onLink(destStat, src, dest, opts, cb) {
428430
// Do not copy if src is a subdir of dest since unlinking
429431
// dest in this case would result in removing src contents
430432
// and therefore a broken symlink would be created.
433+
// TODO(bcoe): I'm having trouble exercising this code in test,
434+
// ask about during review.
431435
if (destStat.isDirectory() &&
432436
stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
433437
return cb(new ERR_FS_COPY_SYMLINK_TO_SUBDIRECTORY({
@@ -440,7 +444,6 @@ function onLink(destStat, src, dest, opts, cb) {
440444
}
441445
return copyLink(resolvedSrc, dest, cb);
442446
});
443-
444447
});
445448
}
446449

test/fixtures/copy/files-and-folders/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/fixtures/copy/files-and-folders/sub-folder/hello.mjs

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Hello
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../README.md

test/fixtures/copy/kitchen-sink/a/c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
b

test/fixtures/copy/symlink/a/b/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/parallel/test-copy.js

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
'use strict';
22

33
const common = require('../common');
4+
if (!common.hasCrypto) { common.skip('missing crypto'); }
45

56
const assert = require('assert');
7+
const { randomUUID } = require('crypto');
68
const fs = require('fs');
79
const {
810
copy,
11+
copySync,
912
lstatSync,
13+
mkdirSync,
1014
readdirSync,
11-
rmSync,
15+
symlinkSync,
1216
} = fs;
17+
const net = require('net');
1318
const { dirname, join } = require('path');
1419

20+
const isWindows = process.platform === 'win32';
1521
const tmpdir = require('../common/tmpdir');
1622
tmpdir.refresh();
1723

@@ -22,62 +28,88 @@ function nextdir() {
2228

2329
// Async version of copy.
2430

25-
// It copies a nested folder structure with files and folders.
31+
// It copies a nested folder structure with files, folders, symlinks.
2632
{
27-
const src = dirname(require.resolve('../fixtures/copy/files-and-folders'));
33+
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
2834
const dest = nextdir();
2935
copy(src, dest, common.mustCall((err) => {
30-
try {
31-
assert.strictEqual(err, null);
32-
assertDirEquivalent(src, dest);
33-
} finally {
34-
rmSync(dest, { force: true, recursive: true });
35-
}
36+
assert.strictEqual(err, null);
37+
assertDirEquivalent(src, dest);
3638
}));
3739
}
3840

39-
// It copies symlink if dereference is false (default value).
41+
// It throws error if existing symlink in dest is in subdirectory src.
42+
// TODO(bcoe): this behavior seemed strange to me, ask about it in review.
4043
{
41-
const src = dirname(require.resolve('../fixtures/copy/symlink'));
44+
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
4245
const dest = nextdir();
46+
copySync(src, dest);
4347
copy(src, dest, common.mustCall((err) => {
44-
assert.strictEqual(err, null);
48+
assert.strictEqual(err.code, 'ERR_FS_COPY_TO_SUBDIRECTORY');
4549
assertDirEquivalent(src, dest);
4650
}));
4751
}
4852

49-
// It copies folder symlink links to, when dereference is true.
53+
// It does not fail if the same directory is copied to dest twice,
54+
// when dereference is true, and overwrite true.
5055
{
51-
const src = dirname(require.resolve('../fixtures/copy/symlink'));
56+
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
5257
const dest = nextdir();
53-
const destFile = join(dest, 'a/b/README.md');
58+
const destFile = join(dest, 'a/b/README2.md');
5459
copy(src, dest, { dereference: true }, common.mustCall((err) => {
5560
assert.strictEqual(err, null);
5661
const stat = lstatSync(destFile);
5762
assert(stat.isFile());
5863
}));
5964
}
6065

66+
// It copies file itself, rather than symlink, when dereference is true.
67+
{
68+
const src = require.resolve('../fixtures/copy/kitchen-sink');
69+
const dest = nextdir();
70+
const destFile = join(dest, 'foo.js');
71+
copy(src, destFile, { dereference: true }, common.mustCall((err) => {
72+
assert.strictEqual(err, null);
73+
const stat = lstatSync(destFile);
74+
assert(stat.isFile());
75+
}));
76+
}
77+
6178
// It returns error when src and dest are identical.
6279
{
63-
const src = dirname(require.resolve('../fixtures/copy/symlink'));
80+
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
6481
copy(src, src, common.mustCall((err) => {
6582
assert.strictEqual(err.code, 'ERR_FS_COPY_TO_SUBDIRECTORY');
6683
}));
6784
}
6885

86+
// It returns error if part of dest path is symlink to src.
87+
{
88+
const src = nextdir();
89+
mkdirSync(join(src, 'a'), { recursive: true });
90+
const dest = nextdir();
91+
// Create symlink in dest pointing to src.
92+
const destLink = join(dest, 'b');
93+
mkdirSync(dest, { recursive: true });
94+
symlinkSync(src, destLink);
95+
copy(src, join(dest, 'b', 'c'), common.mustCall((err) => {
96+
assert.strictEqual(err.code, 'ERR_FS_COPY_TO_SUBDIRECTORY');
97+
}));
98+
}
99+
69100
// It returns error if attempt is made to copy directory to file.
70101
{
71-
const src = dirname(require.resolve('../fixtures/copy/symlink'));
72-
const dest = require.resolve('../fixtures/copy/files-and-folders');
102+
const src = nextdir();
103+
mkdirSync(src, { recursive: true });
104+
const dest = require.resolve('../fixtures/copy/kitchen-sink');
73105
copy(src, dest, common.mustCall((err) => {
74106
assert.strictEqual(err.code, 'ERR_FS_COPY_DIR_TO_NON_DIR');
75107
}));
76108
}
77109

78110
// It allows file to be copied to a file path.
79111
{
80-
const srcFile = require.resolve('../fixtures/copy/files-and-folders');
112+
const srcFile = require.resolve('../fixtures/copy/kitchen-sink');
81113
const destFile = join(nextdir(), 'index.js');
82114
copy(srcFile, destFile, { dereference: true }, common.mustCall((err) => {
83115
assert.strictEqual(err, null);
@@ -88,24 +120,51 @@ function nextdir() {
88120

89121
// It returns error if attempt is made to copy file to directory.
90122
{
91-
const src = require.resolve('../fixtures/copy/symlink');
92-
const dest = dirname(require.resolve('../fixtures/copy/files-and-folders'));
123+
const src = require.resolve('../fixtures/copy/kitchen-sink');
124+
const dest = nextdir();
125+
mkdirSync(dest, { recursive: true });
93126
copy(src, dest, common.mustCall((err) => {
94127
assert.strictEqual(err.code, 'ERR_FS_COPY_NON_DIR_TO_DIR');
95128
}));
96129
}
97130

98131
// It returns error if attempt is made to copy to subdirectory of self.
99132
{
100-
const src = dirname(require.resolve('../fixtures/copy/files-and-folders'));
133+
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
101134
const dest = dirname(
102-
require.resolve('../fixtures/copy/files-and-folders/sub-folder')
135+
require.resolve('../fixtures/copy/kitchen-sink/a')
103136
);
104137
copy(src, dest, common.mustCall((err) => {
105138
assert.strictEqual(err.code, 'ERR_FS_COPY_TO_SUBDIRECTORY');
106139
}));
107140
}
108141

142+
// It returns an error if attempt is made to copy socket.
143+
{
144+
const dest = nextdir();
145+
const sid = randomUUID();
146+
const sock = isWindows ? `\\\\.\\pipe\\${sid}` : `${sid}.sock`;
147+
const server = net.createServer();
148+
server.listen(sock);
149+
copy(sock, dest, common.mustCall((err) => {
150+
assert.strictEqual(err.code, 'ERR_FS_COPY_SOCKET');
151+
server.close();
152+
}));
153+
}
154+
155+
// It copies timestamps from src to dest if preserveTimestamps is true.
156+
{
157+
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
158+
const dest = nextdir();
159+
copy(src, dest, { preserveTimestamps: true }, common.mustCall((err) => {
160+
assert.strictEqual(err, null);
161+
assertDirEquivalent(src, dest);
162+
const srcStat = lstatSync(join(src, 'index.js'));
163+
const destStat = lstatSync(join(dest, 'index.js'));
164+
assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
165+
}));
166+
}
167+
109168
function assertDirEquivalent(dir1, dir2) {
110169
const dir1Entries = [];
111170
collectEntries(dir1, dir1Entries);

0 commit comments

Comments
 (0)