From fe0bfe29313331eb1ef9fecf3c35053ce32def4e Mon Sep 17 00:00:00 2001 From: Mani Maghsoudlou Date: Thu, 19 Oct 2017 03:29:50 -0700 Subject: [PATCH] Rewrite copy to use recursive pattern for dirs, add more tests --- .../copy-prevent-copying-identical.test.js | 192 +++++++++ .../copy-prevent-copying-into-itself.test.js | 372 ++++++++++++++++++ lib/copy/__tests__/copy.test.js | 38 ++ lib/copy/__tests__/ncp/broken-symlink.test.js | 2 +- lib/copy/__tests__/ncp/ncp-error-perm.test.js | 2 +- lib/copy/__tests__/ncp/ncp.test.js | 2 +- lib/copy/__tests__/ncp/symlink.test.js | 2 +- lib/copy/copy.js | 259 ++++++++++-- lib/copy/ncp.js | 234 ----------- lib/move/index.js | 10 +- 10 files changed, 837 insertions(+), 276 deletions(-) create mode 100644 lib/copy/__tests__/copy-prevent-copying-identical.test.js create mode 100644 lib/copy/__tests__/copy-prevent-copying-into-itself.test.js delete mode 100644 lib/copy/ncp.js diff --git a/lib/copy/__tests__/copy-prevent-copying-identical.test.js b/lib/copy/__tests__/copy-prevent-copying-identical.test.js new file mode 100644 index 00000000..64357105 --- /dev/null +++ b/lib/copy/__tests__/copy-prevent-copying-identical.test.js @@ -0,0 +1,192 @@ +'use strict' + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require(process.cwd()) +const klawSync = require('klaw-sync') + +/* global beforeEach, afterEach, describe, it */ + +describe('+ copySync() - prevent copying identical files and dirs', () => { + let TEST_DIR = '' + let src = '' + let dest = '' + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-prevent-copying-identical') + fs.emptyDir(TEST_DIR, done) + }) + + afterEach(done => fs.remove(TEST_DIR, done)) + + it('should return an error if src and dest are the same', done => { + const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_copy_sync') + const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_copy_sync') + + fs.copy(fileSrc, fileDest, err => { + assert.equal(err.message, 'Source and destination must not be the same.') + done() + }) + }) + + // src is directory: + // src is regular, dest is symlink + // src is symlink, dest is regular + // src is symlink, dest is symlink + + describe('> when the source is a directory', () => { + describe(`>> when src is regular and dest is a symlink that points to src`, () => { + it('should not copy and return', done => { + src = path.join(TEST_DIR, 'src') + fs.mkdirsSync(src) + const subdir = path.join(TEST_DIR, 'src', 'subdir') + fs.mkdirsSync(subdir) + fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data') + + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const oldlen = klawSync(src).length + + fs.copy(src, destLink, err => { + assert.ifError(err) + + const newlen = klawSync(src).length + assert.strictEqual(newlen, oldlen) + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + done() + }) + }) + }) + + describe(`>> when src is a symlink that points to a regular dest`, () => { + it('should throw error', done => { + dest = path.join(TEST_DIR, 'dest') + fs.mkdirsSync(dest) + const subdir = path.join(TEST_DIR, 'dest', 'subdir') + fs.mkdirsSync(subdir) + fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data') + + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(dest, srcLink, 'dir') + + const oldlen = klawSync(dest).length + + fs.copy(srcLink, dest, err => { + assert.ok(err) + + // assert nothing copied + const newlen = klawSync(dest).length + assert.strictEqual(newlen, oldlen) + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + done() + }) + }) + }) + + describe('>> when src and dest are symlinks that point to the exact same path', () => { + it('should not copy and return', done => { + src = path.join(TEST_DIR, 'src') + fs.mkdirsSync(src) + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'dir') + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(srcLink).length + const destlenBefore = klawSync(destLink).length + + fs.copy(srcLink, destLink, err => { + assert.ifError(err) + + const srclenAfter = klawSync(srcLink).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const destlenAfter = klawSync(destLink).length + assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change') + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + done() + }) + }) + }) + }) + + // src is file: + // src is regular, dest is symlink + // src is symlink, dest is regular + // src is symlink, dest is symlink + + describe('> when the source is a file', () => { + describe(`>> when src is regular and dest is a symlink that points to src`, () => { + it('should not copy and return', done => { + src = path.join(TEST_DIR, 'src', 'somefile.txt') + fs.ensureFileSync(src) + fs.writeFileSync(src, 'some data') + + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'file') + + fs.copy(src, destLink, err => { + assert.ifError(err) + + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + assert(fs.readFileSync(link, 'utf8'), 'some data') + done() + }) + }) + }) + + describe(`>> when src is a symlink that points to a regular dest`, () => { + it('should throw error', done => { + dest = path.join(TEST_DIR, 'dest', 'somefile.txt') + fs.ensureFileSync(dest) + fs.writeFileSync(dest, 'some data') + + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(dest, srcLink, 'file') + + fs.copy(srcLink, dest, err => { + assert.ok(err) + + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, dest) + assert(fs.readFileSync(link, 'utf8'), 'some data') + done() + }) + }) + }) + + describe('>> when src and dest are symlinks that point to the exact same path', () => { + it('should not copy and return', done => { + src = path.join(TEST_DIR, 'src', 'srcfile.txt') + fs.ensureFileSync(src) + fs.writeFileSync(src, 'src data') + + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'file') + + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(src, destLink, 'file') + + fs.copy(srcLink, destLink, err => { + assert.ifError(err) + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + assert(fs.readFileSync(srcln, 'utf8'), 'src data') + assert(fs.readFileSync(destln, 'utf8'), 'src data') + done() + }) + }) + }) + }) +}) diff --git a/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js new file mode 100644 index 00000000..d759a1a5 --- /dev/null +++ b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js @@ -0,0 +1,372 @@ +'use strict' + +const assert = require('assert') +const os = require('os') +const path = require('path') +const fs = require(process.cwd()) +const klawSync = require('klaw-sync') + +/* global beforeEach, afterEach, describe, it */ + +// these files are used for all tests +const FILES = [ + 'file0.txt', + path.join('dir1', 'file1.txt'), + path.join('dir1', 'dir2', 'file2.txt'), + path.join('dir1', 'dir2', 'dir3', 'file3.txt') +] + +const dat0 = 'file0' +const dat1 = 'file1' +const dat2 = 'file2' +const dat3 = 'file3' + +function testSuccess (src, dest, done) { + const srclen = klawSync(src).length + assert(srclen > 2) + fs.copy(src, dest, err => { + assert.ifError(err) + + const destlen = klawSync(dest).length + + assert.strictEqual(destlen, srclen) + + FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied')) + + const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8') + const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8') + const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8') + const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8') + + assert.strictEqual(o0, dat0, 'file contents matched') + assert.strictEqual(o1, dat1, 'file contents matched') + assert.strictEqual(o2, dat2, 'file contents matched') + assert.strictEqual(o3, dat3, 'file contents matched') + done() + }) +} + +function testError (src, dest, done) { + fs.copy(src, dest, err => { + assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`) + done() + }) +} + +describe('+ copy() - prevent copying into itself', () => { + let TEST_DIR, src + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-prevent-copying-into-itself-4') + src = path.join(TEST_DIR, 'src') + fs.mkdirpSync(src) + + fs.outputFileSync(path.join(src, FILES[0]), dat0) + fs.outputFileSync(path.join(src, FILES[1]), dat1) + fs.outputFileSync(path.join(src, FILES[2]), dat2) + fs.outputFileSync(path.join(src, FILES[3]), dat3) + done() + }) + + afterEach(done => fs.remove(TEST_DIR, done)) + + describe('> when source is a file', () => { + it('should copy the file successfully even when dest parent is a subdir of src', done => { + const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt') + const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt') + fs.writeFileSync(srcFile, dat0) + + fs.copy(srcFile, destFile, err => { + assert.ifError(err) + + assert(fs.existsSync(destFile, 'file copied')) + const out = fs.readFileSync(destFile, 'utf8') + assert.strictEqual(out, dat0, 'file contents matched') + done() + }) + }) + }) + + // test for these cases: + // - src is directory, dest is directory + // - src is directory, dest is symlink + // - src is symlink, dest is directory + // - src is symlink, dest is symlink + + describe('> when source is a directory', () => { + describe('>> when dest is a directory', () => { + it(`should copy the directory successfully when dest is 'src_dest'`, done => { + const dest = path.join(TEST_DIR, 'src_dest') + return testSuccess(src, dest, done) + }) + it(`should copy the directory successfully when dest is 'src-dest'`, done => { + const dest = path.join(TEST_DIR, 'src-dest') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'dest_src'`, done => { + const dest = path.join(TEST_DIR, 'dest_src') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'src_dest/src'`, done => { + const dest = path.join(TEST_DIR, 'src_dest', 'src') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'src-dest/src'`, done => { + const dest = path.join(TEST_DIR, 'src-dest', 'src') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'dest_src/src'`, done => { + const dest = path.join(TEST_DIR, 'dest_src', 'src') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'src_src/dest'`, done => { + const dest = path.join(TEST_DIR, 'src_src', 'dest') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'src-src/dest'`, done => { + const dest = path.join(TEST_DIR, 'src-src', 'dest') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => { + const dest = path.join(TEST_DIR, 'srcsrc', 'dest') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'dest/src'`, done => { + const dest = path.join(TEST_DIR, 'dest', 'src') + return testSuccess(src, dest, done) + }) + + it('should copy the directory successfully when dest is very nested that all its parents need to be created', done => { + const dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo', + 'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply', + 'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest') + return testSuccess(src, dest, done) + }) + + it(`should error when dest is 'src/dest'`, done => { + const dest = path.join(TEST_DIR, 'src', 'dest') + return testError(src, dest, done) + }) + + it(`should error when dest is 'src/src_dest'`, done => { + const dest = path.join(TEST_DIR, 'src', 'src_dest') + return testError(src, dest, done) + }) + + it(`should error when dest is 'src/dest_src'`, done => { + const dest = path.join(TEST_DIR, 'src', 'dest_src') + return testError(src, dest, done) + }) + + it(`should error when dest is 'src/dest/src'`, done => { + const dest = path.join(TEST_DIR, 'src', 'dest', 'src') + return testError(src, dest, done) + }) + }) + + describe('>> when dest is a symlink', () => { + it('should not copy and return when dest points exactly to src', done => { + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(src).length + assert(srclenBefore > 2) + + fs.copy(src, destLink, err => { + assert.ifError(err) + + const srclenAfter = klawSync(src).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + + const link = fs.readlinkSync(destLink) + assert.strictEqual(link, src) + done() + }) + }) + + it('should copy the directory successfully when src is a subdir of resolved dest path', done => { + const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src') + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.copySync(src, srcInDest) // put some stuff in srcInDest + + const dest = path.join(TEST_DIR, 'dest') + fs.symlinkSync(dest, destLink, 'dir') + + const srclen = klawSync(srcInDest).length + const destlenBefore = klawSync(dest).length + + assert(srclen > 2) + fs.copy(srcInDest, destLink, err => { + assert.ifError(err) + + const destlenAfter = klawSync(dest).length + + // assert dest length is oldlen + length of stuff copied from src + assert.strictEqual(destlenAfter, destlenBefore + srclen, 'dest length should be equal to old length + copied legnth') + + FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied')) + + const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8') + const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8') + const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8') + const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8') + + assert.strictEqual(o0, dat0, 'files contents matched') + assert.strictEqual(o1, dat1, 'files contents matched') + assert.strictEqual(o2, dat2, 'files contents matched') + assert.strictEqual(o3, dat3, 'files contents matched') + done() + }) + }) + }) + }) + + describe('> when source is a symlink', () => { + describe('>> when dest is a directory', () => { + it('should error when resolved src path points to dest', done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src') + + fs.copy(srcLink, dest, err => { + assert(err) + // assert source not affected + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, src) + done() + }) + }) + + it('should error when dest is a subdir of resolved src path', done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest') + fs.mkdirsSync(dest) + + fs.copy(srcLink, dest, err => { + assert(err) + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, src) + done() + }) + }) + + it('should error when resolved src path is a subdir of dest', done => { + const dest = path.join(TEST_DIR, 'dest') + + const resolvedSrcPath = path.join(dest, 'contains', 'src') + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.copySync(src, resolvedSrcPath) + + // make symlink that points to a subdir in dest + fs.symlinkSync(resolvedSrcPath, srcLink, 'dir') + + fs.copy(srcLink, dest, err => { + assert(err) + done() + }) + }) + + it(`should copy the directory successfully when dest is 'src_src/dest'`, done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'src_src', 'dest') + testSuccess(srcLink, dest, () => { + const link = fs.readlinkSync(dest) + assert.strictEqual(link, src) + done() + }) + }) + + it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const dest = path.join(TEST_DIR, 'srcsrc', 'dest') + testSuccess(srcLink, dest, () => { + const link = fs.readlinkSync(dest) + assert.strictEqual(link, src) + done() + }) + }) + }) + + describe('>> when dest is a symlink', () => { + it('should silently return when resolved dest path is exactly the same as resolved src path', done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + const destLink = path.join(TEST_DIR, 'dest-symlink') + fs.symlinkSync(src, destLink, 'dir') + + const srclenBefore = klawSync(srcLink).length + const destlenBefore = klawSync(destLink).length + assert(srclenBefore > 2) + assert(destlenBefore > 2) + + fs.copy(srcLink, destLink, err => { + assert.ifError(err) + + const srclenAfter = klawSync(srcLink).length + assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change') + const destlenAfter = klawSync(destLink).length + assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change') + + const srcln = fs.readlinkSync(srcLink) + assert.strictEqual(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + done() + }) + }) + + it('should error when resolved dest path is a subdir of resolved src path', done => { + const srcLink = path.join(TEST_DIR, 'src-symlink') + fs.symlinkSync(src, srcLink, 'dir') + + const destLink = path.join(TEST_DIR, 'dest-symlink') + const resolvedDestPath = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest') + fs.ensureFileSync(path.join(resolvedDestPath, 'subdir', 'somefile.txt')) + + fs.symlinkSync(resolvedDestPath, destLink, 'dir') + + fs.copy(srcLink, destLink, err => { + assert.ifError(err) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, src) + done() + }) + }) + + it('should error when resolved src path is a subdir of resolved dest path', done => { + const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src') + const srcLink = path.join(TEST_DIR, 'src-symlink') + const destLink = path.join(TEST_DIR, 'dest-symlink') + + const dest = path.join(TEST_DIR, 'dest') + + fs.mkdirSync(dest) + + fs.symlinkSync(srcInDest, srcLink, 'dir') + fs.symlinkSync(dest, destLink, 'dir') + + fs.copy(srcLink, destLink, err => { + assert.strictEqual(err.message, `Cannot overwrite '${dest}' with '${srcInDest}'.`) + const destln = fs.readlinkSync(destLink) + assert.strictEqual(destln, dest) + done() + }) + }) + }) + }) +}) diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js index 6ed64ff8..4787bfd8 100644 --- a/lib/copy/__tests__/copy.test.js +++ b/lib/copy/__tests__/copy.test.js @@ -30,6 +30,30 @@ describe('fs-extra', () => { }) }) + it('should error when overwrite=false and file exists', done => { + const src = path.join(TEST_DIR, 'src.txt') + const dest = path.join(TEST_DIR, 'dest.txt') + + fse.ensureFileSync(src) + fse.ensureFileSync(dest) + fse.copy(src, dest, {overwrite: false, errorOnExist: true}, err => { + assert(err) + done() + }) + }) + + it('should error when overwrite=false and file exists in a dir', done => { + const src = path.join(TEST_DIR, 'src', 'sfile.txt') + const dest = path.join(TEST_DIR, 'dest', 'dfile.txt') + + fse.ensureFileSync(src) + fse.ensureFileSync(dest) + fse.copy(src, dest, {overwrite: false, errorOnExist: true}, err => { + assert(err) + done() + }) + }) + describe('> when the source is a file', () => { it('should copy the file asynchronously', done => { const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_src') @@ -98,6 +122,20 @@ describe('fs-extra', () => { }) }) + describe('> when dest exists and is a file', () => { + it('should return an error', done => { + const src = path.join(TEST_DIR, 'src') + const dest = path.join(TEST_DIR, 'file.txt') + fs.mkdirSync(src) + fse.ensureFileSync(dest) + + fse.copy(src, dest, err => { + assert.strictEqual(err.message, `Cannot overwrite non-directory '${dest}' with directory '${src}'.`) + done() + }) + }) + }) + it('should copy the directory asynchronously', done => { const FILES = 2 const src = path.join(TEST_DIR, 'src') diff --git a/lib/copy/__tests__/ncp/broken-symlink.test.js b/lib/copy/__tests__/ncp/broken-symlink.test.js index 781ac6d9..278b2c11 100644 --- a/lib/copy/__tests__/ncp/broken-symlink.test.js +++ b/lib/copy/__tests__/ncp/broken-symlink.test.js @@ -3,7 +3,7 @@ const fs = require('fs') const os = require('os') const fse = require(process.cwd()) -const ncp = require('../../ncp') +const ncp = require('../../copy') const path = require('path') const assert = require('assert') diff --git a/lib/copy/__tests__/ncp/ncp-error-perm.test.js b/lib/copy/__tests__/ncp/ncp-error-perm.test.js index 451bbdf7..18876e69 100644 --- a/lib/copy/__tests__/ncp/ncp-error-perm.test.js +++ b/lib/copy/__tests__/ncp/ncp-error-perm.test.js @@ -5,7 +5,7 @@ const fs = require('fs') const os = require('os') const fse = require(process.cwd()) -const ncp = require('../../ncp') +const ncp = require('../../copy') const path = require('path') const assert = require('assert') diff --git a/lib/copy/__tests__/ncp/ncp.test.js b/lib/copy/__tests__/ncp/ncp.test.js index 23ad3fcc..31ae8296 100644 --- a/lib/copy/__tests__/ncp/ncp.test.js +++ b/lib/copy/__tests__/ncp/ncp.test.js @@ -1,7 +1,7 @@ 'use strict' const fs = require('fs') -const ncp = require('../../ncp') +const ncp = require('../../copy') const path = require('path') const rimraf = require('rimraf') const assert = require('assert') diff --git a/lib/copy/__tests__/ncp/symlink.test.js b/lib/copy/__tests__/ncp/symlink.test.js index 1b8816b6..ff481a5b 100644 --- a/lib/copy/__tests__/ncp/symlink.test.js +++ b/lib/copy/__tests__/ncp/symlink.test.js @@ -3,7 +3,7 @@ const fs = require('fs') const os = require('os') const fse = require(process.cwd()) -const ncp = require('../../ncp') +const ncp = require('../../copy') const path = require('path') const assert = require('assert') diff --git a/lib/copy/copy.js b/lib/copy/copy.js index 309a93df..1aa53228 100644 --- a/lib/copy/copy.js +++ b/lib/copy/copy.js @@ -2,53 +2,246 @@ const fs = require('graceful-fs') const path = require('path') -const ncp = require('./ncp') -const mkdir = require('../mkdirs') +const mkdirp = require('../mkdirs').mkdirs const pathExists = require('../path-exists').pathExists +const utimes = require('../util/utimes').utimesMillis -function copy (src, dest, options, callback) { - if (typeof options === 'function' && !callback) { - callback = options - options = {} - } else if (typeof options === 'function' || options instanceof RegExp) { - options = {filter: options} +const notExist = Symbol('notExist') +const existsReg = Symbol('existsReg') + +function copy (src, dest, opts, cb) { + if (typeof opts === 'function' && !cb) { + cb = opts + opts = {} + } else if (typeof opts === 'function' || opts instanceof RegExp) { + opts = {filter: opts} } - callback = callback || function () {} - options = options || {} - // Warn about using preserveTimestamps on 32-bit node: - if (options.preserveTimestamps && process.arch === 'ia32') { + cb = cb || function () {} + opts = opts || {} + + opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now + opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber + + // Warn about using preserveTimestamps on 32-bit node + if (opts.preserveTimestamps && process.arch === 'ia32') { console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n see https://github.com/jprichardson/node-fs-extra/issues/269`) } + src = path.resolve(src) + dest = path.resolve(dest) + // don't allow src and dest to be the same - const basePath = process.cwd() - const currentPath = path.resolve(basePath, src) - const targetPath = path.resolve(basePath, dest) - if (currentPath === targetPath) return callback(new Error('Source and destination must not be the same.')) - - fs.lstat(src, (err, stats) => { - if (err) return callback(err) - - let dir = null - if (stats.isDirectory()) { - const parts = dest.split(path.sep) - parts.pop() - dir = parts.join(path.sep) + if (src === dest) return cb(new Error('Source and destination must not be the same.')) + + const destParent = path.dirname(dest) + pathExists(destParent, (err, dirExists) => { + if (err) return cb(err) + if (dirExists) return startCopy(src, dest, opts, cb) + mkdirp(destParent, err => { + if (err) return cb(err) + return startCopy(src, dest, opts, cb) + }) + }) +} + +function startCopy (src, dest, opts, cb) { + if (opts.filter) { + if (opts.filter instanceof RegExp) { + console.warn('Warning: fs-extra: Passing a RegExp filter is deprecated, use a function') + if (!opts.filter.test(src)) return cb() + } else if (typeof opts.filter === 'function') { + if (!opts.filter(src, dest)) return cb() + } + } + return getStats(src, dest, opts, cb) +} + +function getStats (src, dest, opts, cb) { + const stat = opts.dereference ? fs.stat : fs.lstat + stat(src, (err, st) => { + if (err) return cb(err) + + if (st.isDirectory()) return onDir(st, src, dest, opts, cb) + else if (st.isFile() || + st.isCharacterDevice() || + st.isBlockDevice()) return onFile(st, src, dest, opts, cb) + else if (st.isSymbolicLink()) return onLink(src, dest, opts, cb) + }) +} + +function onFile (srcStat, src, dest, opts, cb) { + checkDest(dest, (err, resolvedPath) => { + if (err) return cb(err) + if (resolvedPath === notExist) { + return cpFile(srcStat, src, dest, opts, cb) + } else if (resolvedPath === existsReg) { + return mayCopyFile(srcStat, src, dest, opts, cb) } else { - dir = path.dirname(dest) + if (src === resolvedPath) return cb() + return mayCopyFile(srcStat, src, dest, opts, cb) } + }) +} + +function mayCopyFile (srcStat, src, dest, opts, cb) { + if (opts.overwrite) { + fs.unlink(dest, err => { + if (err) return cb(err) + return cpFile(srcStat, src, dest, opts, cb) + }) + } else if (opts.errorOnExist) { + return cb(new Error(`'${dest}' already exists`)) + } else return cb() +} + +function cpFile (srcStat, src, dest, opts, cb) { + const rs = fs.createReadStream(src) + const ws = fs.createWriteStream(dest, { mode: srcStat.mode }) - pathExists(dir, (err, dirExists) => { - if (err) return callback(err) - if (dirExists) return ncp(src, dest, options, callback) - mkdir.mkdirs(dir, err => { - if (err) return callback(err) - ncp(src, dest, options, callback) - }) + rs.on('error', err => cb(err)) + ws.on('error', err => cb(err)) + + ws.on('open', () => { + rs.pipe(ws) + }).once('close', () => { + fs.chmod(dest, srcStat.mode, err => { + if (err) return cb(err) + if (opts.preserveTimestamps) { + return utimes(dest, srcStat.atime, srcStat.mtime, cb) + } + return cb() }) }) } +function onDir (srcStat, src, dest, opts, cb) { + checkDest(dest, (err, resolvedPath) => { + if (err) return cb(err) + if (resolvedPath === notExist) { + if (isSrcSubdir(src, dest)) { + return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)) + } + return mkDirAndCopy(srcStat, src, dest, opts, cb) + } else if (resolvedPath === existsReg) { + if (isSrcSubdir(src, dest)) { + return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)) + } + return mayCopyDir(src, dest, opts, cb) + } else { + if (src === resolvedPath) return cb() + return cpDir(src, dest, opts, cb) + } + }) +} + +function mayCopyDir (src, dest, opts, cb) { + fs.stat(dest, (err, st) => { + if (err) return cb(err) + if (!st.isDirectory()) { + return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)) + } + return cpDir(src, dest, opts, cb) + }) +} + +function mkDirAndCopy (srcStat, src, dest, opts, cb) { + fs.mkdir(dest, srcStat.mode, err => { + if (err) return cb(err) + fs.chmod(dest, srcStat.mode, err => { + if (err) return cb(err) + return cpDir(src, dest, opts, cb) + }) + }) +} + +function cpDir (src, dest, opts, cb) { + fs.readdir(src, (err, items) => { + if (err) return cb(err) + return cpDirItems(items, src, dest, opts, cb) + }) +} + +function cpDirItems (items, src, dest, opts, cb) { + const item = items.pop() + if (!item) return cb() + startCopy(path.join(src, item), path.join(dest, item), opts, err => { + if (err) return cb(err) + return cpDirItems(items, src, dest, opts, cb) + }) +} + +function onLink (src, dest, opts, cb) { + fs.readlink(src, (err, resolvedSrcPath) => { + if (err) return cb(err) + + if (opts.dereference) { + resolvedSrcPath = path.resolve(process.cwd(), resolvedSrcPath) + } + + checkDest(dest, (err, resolvedDestPath) => { + if (err) return cb(err) + + if (resolvedDestPath === notExist || resolvedDestPath === existsReg) { + // if dest already exists, fs throws error anyway, + // so no need to guard against it here. + return fs.symlink(resolvedSrcPath, dest, cb) + } else { + if (opts.dereference) { + resolvedDestPath = path.resolve(process.cwd(), resolvedDestPath) + } + if (resolvedDestPath === resolvedSrcPath) return cb() + + // prevent copy if src is a subdir of dest since unlinking + // dest in this case results in removing src contents + // and therefore a broken symlink will be created. + fs.stat(dest, (err, st) => { + if (err) return cb(err) + if (st.isDirectory() && isSrcSubdir(resolvedDestPath, resolvedSrcPath)) { + return cb(new Error(`Cannot overwrite '${resolvedDestPath}' with '${resolvedSrcPath}'.`)) + } + return cpLink(resolvedSrcPath, dest, cb) + }) + } + }) + }) +} + +function cpLink (resolvedSrcPath, dest, cb) { + fs.unlink(dest, err => { + if (err) return cb(err) + return fs.symlink(resolvedSrcPath, dest, cb) + }) +} + +// check dest to see if it exists and/or is a symlink +function checkDest (dest, cb) { + fs.readlink(dest, (err, resolvedPath) => { + if (err) { + if (err.code === 'ENOENT') return cb(null, notExist) + + // dest exists and is a regular file or directory, Windows throws UNKNOWN error. + if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return cb(null, existsReg) + + return cb(err) + } + return cb(null, resolvedPath) // dest exists and is a symlink + }) +} + +// return true if dest is a subdir of src, otherwise false. +// extract dest base dir and check if that is the same as src basename +function isSrcSubdir (src, dest) { + const baseDir = dest.split(path.dirname(src) + path.sep)[1] + if (baseDir) { + const destBasename = baseDir.split(path.sep)[0] + if (destBasename) { + return src !== dest && dest.indexOf(src) > -1 && destBasename === path.basename(src) + } + return false + } + return false +} + module.exports = copy diff --git a/lib/copy/ncp.js b/lib/copy/ncp.js deleted file mode 100644 index 9670ee02..00000000 --- a/lib/copy/ncp.js +++ /dev/null @@ -1,234 +0,0 @@ -// imported from ncp (this is temporary, will rewrite) - -var fs = require('graceful-fs') -var path = require('path') -var utimes = require('../util/utimes') - -function ncp (source, dest, options, callback) { - if (!callback) { - callback = options - options = {} - } - - var basePath = process.cwd() - var currentPath = path.resolve(basePath, source) - var targetPath = path.resolve(basePath, dest) - - var filter = options.filter - var transform = options.transform - var overwrite = options.overwrite - // If overwrite is undefined, use clobber, otherwise default to true: - if (overwrite === undefined) overwrite = options.clobber - if (overwrite === undefined) overwrite = true - var errorOnExist = options.errorOnExist - var dereference = options.dereference - var preserveTimestamps = options.preserveTimestamps === true - - var started = 0 - var finished = 0 - var running = 0 - - var errored = false - - startCopy(currentPath) - - function startCopy (source) { - started++ - if (filter) { - if (filter instanceof RegExp) { - console.warn('Warning: fs-extra: Passing a RegExp filter is deprecated, use a function') - if (!filter.test(source)) { - return doneOne(true) - } - } else if (typeof filter === 'function') { - if (!filter(source, dest)) { - return doneOne(true) - } - } - } - return getStats(source) - } - - function getStats (source) { - var stat = dereference ? fs.stat : fs.lstat - running++ - stat(source, function (err, stats) { - if (err) return onError(err) - - // We need to get the mode from the stats object and preserve it. - var item = { - name: source, - mode: stats.mode, - mtime: stats.mtime, // modified time - atime: stats.atime, // access time - stats: stats // temporary - } - - if (stats.isDirectory()) { - return onDir(item) - } else if (stats.isFile() || stats.isCharacterDevice() || stats.isBlockDevice()) { - return onFile(item) - } else if (stats.isSymbolicLink()) { - // Symlinks don't really need to know about the mode. - return onLink(source) - } - }) - } - - function onFile (file) { - var target = file.name.replace(currentPath, targetPath.replace('$', '$$$$')) // escapes '$' with '$$' - isWritable(target, function (writable) { - if (writable) { - copyFile(file, target) - } else { - if (overwrite) { - rmFile(target, function () { - copyFile(file, target) - }) - } else if (errorOnExist) { - onError(new Error(target + ' already exists')) - } else { - doneOne() - } - } - }) - } - - function copyFile (file, target) { - var readStream = fs.createReadStream(file.name) - var writeStream = fs.createWriteStream(target, { mode: file.mode }) - - readStream.on('error', onError) - writeStream.on('error', onError) - - if (transform) { - transform(readStream, writeStream, file) - } else { - writeStream.on('open', function () { - readStream.pipe(writeStream) - }) - } - - writeStream.once('close', function () { - fs.chmod(target, file.mode, function (err) { - if (err) return onError(err) - if (preserveTimestamps) { - utimes.utimesMillis(target, file.atime, file.mtime, function (err) { - if (err) return onError(err) - return doneOne() - }) - } else { - doneOne() - } - }) - }) - } - - function rmFile (file, done) { - fs.unlink(file, function (err) { - if (err) return onError(err) - return done() - }) - } - - function onDir (dir) { - var target = dir.name.replace(currentPath, targetPath.replace('$', '$$$$')) // escapes '$' with '$$' - isWritable(target, function (writable) { - if (writable) { - return mkDir(dir, target) - } - copyDir(dir.name) - }) - } - - function mkDir (dir, target) { - fs.mkdir(target, dir.mode, function (err) { - if (err) return onError(err) - // despite setting mode in fs.mkdir, doesn't seem to work - // so we set it here. - fs.chmod(target, dir.mode, function (err) { - if (err) return onError(err) - copyDir(dir.name) - }) - }) - } - - function copyDir (dir) { - fs.readdir(dir, function (err, items) { - if (err) return onError(err) - items.forEach(function (item) { - startCopy(path.join(dir, item)) - }) - return doneOne() - }) - } - - function onLink (link) { - var target = link.replace(currentPath, targetPath) - fs.readlink(link, function (err, resolvedPath) { - if (err) return onError(err) - checkLink(resolvedPath, target) - }) - } - - function checkLink (resolvedPath, target) { - if (dereference) { - resolvedPath = path.resolve(basePath, resolvedPath) - } - isWritable(target, function (writable) { - if (writable) { - return makeLink(resolvedPath, target) - } - fs.readlink(target, function (err, targetDest) { - if (err) return onError(err) - - if (dereference) { - targetDest = path.resolve(basePath, targetDest) - } - if (targetDest === resolvedPath) { - return doneOne() - } - return rmFile(target, function () { - makeLink(resolvedPath, target) - }) - }) - }) - } - - function makeLink (linkPath, target) { - fs.symlink(linkPath, target, function (err) { - if (err) return onError(err) - return doneOne() - }) - } - - function isWritable (path, done) { - fs.lstat(path, function (err) { - if (err) { - if (err.code === 'ENOENT') return done(true) - return done(false) - } - return done(false) - }) - } - - function onError (err) { - // ensure callback is defined & called only once: - if (!errored && callback !== undefined) { - errored = true - return callback(err) - } - } - - function doneOne (skipped) { - if (!skipped) running-- - finished++ - if ((started === finished) && (running === 0)) { - if (callback !== undefined) { - return callback(null) - } - } - } -} - -module.exports = ncp diff --git a/lib/move/index.js b/lib/move/index.js index eeeb30fe..a7181351 100644 --- a/lib/move/index.js +++ b/lib/move/index.js @@ -8,7 +8,7 @@ const u = require('universalify').fromCallback const fs = require('graceful-fs') -const ncp = require('../copy/ncp') +const copy = require('../copy/copy') const path = require('path') const remove = require('../remove').remove const mkdirp = require('../mkdirs').mkdirs @@ -133,14 +133,14 @@ function moveDirAcrossDevice (src, dest, overwrite, callback) { if (overwrite) { remove(dest, err => { if (err) return callback(err) - startNcp() + startCopy() }) } else { - startNcp() + startCopy() } - function startNcp () { - ncp(src, dest, options, err => { + function startCopy () { + copy(src, dest, options, err => { if (err) return callback(err) remove(src, callback) })