Skip to content

Commit

Permalink
fs: add recursive copy method
Browse files Browse the repository at this point in the history
Introduces recursive copy method, based on fs-extra implementation

Refs: nodejs/tooling#98
  • Loading branch information
bcoe committed Jul 13, 2021
1 parent d9f270b commit bb5fe4a
Show file tree
Hide file tree
Showing 11 changed files with 89 additions and 29 deletions.
5 changes: 4 additions & 1 deletion lib/internal/fs/copy/copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,8 @@ function copyDirItem(items, item, src, dest, opts, cb) {
function onLink(destStat, src, dest, opts, cb) {
readlink(src, (err, resolvedSrc) => {
if (err) return cb(err);
// TODO(bcoe): I don't know how this could be called, because
// getStatsForCopy will have used stat. Ask during review.
if (opts.dereference) {
resolvedSrc = resolve(process.cwd(), resolvedSrc);
}
Expand Down Expand Up @@ -428,6 +430,8 @@ function onLink(destStat, src, dest, opts, cb) {
// Do not copy if src is a subdir of dest since unlinking
// dest in this case would result in removing src contents
// and therefore a broken symlink would be created.
// TODO(bcoe): I'm having trouble exercising this code in test,
// ask about during review.
if (destStat.isDirectory() &&
stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
return cb(new ERR_FS_COPY_SYMLINK_TO_SUBDIRECTORY({
Expand All @@ -440,7 +444,6 @@ function onLink(destStat, src, dest, opts, cb) {
}
return copyLink(resolvedSrc, dest, cb);
});

});
}

Expand Down
1 change: 0 additions & 1 deletion test/fixtures/copy/files-and-folders/README.md

This file was deleted.

3 changes: 0 additions & 3 deletions test/fixtures/copy/files-and-folders/sub-folder/hello.mjs

This file was deleted.

1 change: 1 addition & 0 deletions test/fixtures/copy/kitchen-sink/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Hello
1 change: 1 addition & 0 deletions test/fixtures/copy/kitchen-sink/a/b/README2.md
1 change: 1 addition & 0 deletions test/fixtures/copy/kitchen-sink/a/c
File renamed without changes.
File renamed without changes.
1 change: 0 additions & 1 deletion test/fixtures/copy/symlink/a/b/README.md

This file was deleted.

105 changes: 82 additions & 23 deletions test/parallel/test-copy.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
'use strict';

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

const assert = require('assert');
const { randomUUID } = require('crypto');
const fs = require('fs');
const {
copy,
copySync,
lstatSync,
mkdirSync,
readdirSync,
rmSync,
symlinkSync,
} = fs;
const net = require('net');
const { dirname, join } = require('path');

const isWindows = process.platform === 'win32';
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();

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

// Async version of copy.

// It copies a nested folder structure with files and folders.
// It copies a nested folder structure with files, folders, symlinks.
{
const src = dirname(require.resolve('../fixtures/copy/files-and-folders'));
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
const dest = nextdir();
copy(src, dest, common.mustCall((err) => {
try {
assert.strictEqual(err, null);
assertDirEquivalent(src, dest);
} finally {
rmSync(dest, { force: true, recursive: true });
}
assert.strictEqual(err, null);
assertDirEquivalent(src, dest);
}));
}

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

// It copies folder symlink links to, when dereference is true.
// It does not fail if the same directory is copied to dest twice,
// when dereference is true, and overwrite true.
{
const src = dirname(require.resolve('../fixtures/copy/symlink'));
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
const dest = nextdir();
const destFile = join(dest, 'a/b/README.md');
const destFile = join(dest, 'a/b/README2.md');
copy(src, dest, { dereference: true }, common.mustCall((err) => {
assert.strictEqual(err, null);
const stat = lstatSync(destFile);
assert(stat.isFile());
}));
}

// It copies file itself, rather than symlink, when dereference is true.
{
const src = require.resolve('../fixtures/copy/kitchen-sink');
const dest = nextdir();
const destFile = join(dest, 'foo.js');
copy(src, destFile, { dereference: true }, common.mustCall((err) => {
assert.strictEqual(err, null);
const stat = lstatSync(destFile);
assert(stat.isFile());
}));
}

// It returns error when src and dest are identical.
{
const src = dirname(require.resolve('../fixtures/copy/symlink'));
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
copy(src, src, common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_FS_COPY_TO_SUBDIRECTORY');
}));
}

// It returns error if part of dest path is symlink to src.
{
const src = nextdir();
mkdirSync(join(src, 'a'), { recursive: true });
const dest = nextdir();
// Create symlink in dest pointing to src.
const destLink = join(dest, 'b');
mkdirSync(dest, { recursive: true });
symlinkSync(src, destLink);
copy(src, join(dest, 'b', 'c'), common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_FS_COPY_TO_SUBDIRECTORY');
}));
}

// It returns error if attempt is made to copy directory to file.
{
const src = dirname(require.resolve('../fixtures/copy/symlink'));
const dest = require.resolve('../fixtures/copy/files-and-folders');
const src = nextdir();
mkdirSync(src, { recursive: true });
const dest = require.resolve('../fixtures/copy/kitchen-sink');
copy(src, dest, common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_FS_COPY_DIR_TO_NON_DIR');
}));
}

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

// It returns error if attempt is made to copy file to directory.
{
const src = require.resolve('../fixtures/copy/symlink');
const dest = dirname(require.resolve('../fixtures/copy/files-and-folders'));
const src = require.resolve('../fixtures/copy/kitchen-sink');
const dest = nextdir();
mkdirSync(dest, { recursive: true });
copy(src, dest, common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_FS_COPY_NON_DIR_TO_DIR');
}));
}

// It returns error if attempt is made to copy to subdirectory of self.
{
const src = dirname(require.resolve('../fixtures/copy/files-and-folders'));
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
const dest = dirname(
require.resolve('../fixtures/copy/files-and-folders/sub-folder')
require.resolve('../fixtures/copy/kitchen-sink/a')
);
copy(src, dest, common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_FS_COPY_TO_SUBDIRECTORY');
}));
}

// It returns an error if attempt is made to copy socket.
{
const dest = nextdir();
const sid = randomUUID();
const sock = isWindows ? `\\\\.\\pipe\\${sid}` : `${sid}.sock`;
const server = net.createServer();
server.listen(sock);
copy(sock, dest, common.mustCall((err) => {
assert.strictEqual(err.code, 'ERR_FS_COPY_SOCKET');
server.close();
}));
}

// It copies timestamps from src to dest if preserveTimestamps is true.
{
const src = dirname(require.resolve('../fixtures/copy/kitchen-sink'));
const dest = nextdir();
copy(src, dest, { preserveTimestamps: true }, common.mustCall((err) => {
assert.strictEqual(err, null);
assertDirEquivalent(src, dest);
const srcStat = lstatSync(join(src, 'index.js'));
const destStat = lstatSync(join(dest, 'index.js'));
assert.strictEqual(srcStat.mtime.getTime(), destStat.mtime.getTime());
}));
}

function assertDirEquivalent(dir1, dir2) {
const dir1Entries = [];
collectEntries(dir1, dir1Entries);
Expand Down

0 comments on commit bb5fe4a

Please sign in to comment.