diff --git a/lib/copy/__tests__/ncp/broken-symlink.test.js b/lib/copy/__tests__/copy-broken-symlink.test.js similarity index 76% rename from lib/copy/__tests__/ncp/broken-symlink.test.js rename to lib/copy/__tests__/copy-broken-symlink.test.js index 781ac6d9..b598acfc 100644 --- a/lib/copy/__tests__/ncp/broken-symlink.test.js +++ b/lib/copy/__tests__/copy-broken-symlink.test.js @@ -3,14 +3,14 @@ const fs = require('fs') const os = require('os') const fse = require(process.cwd()) -const ncp = require('../../ncp') const path = require('path') const assert = require('assert') +const copy = require('../copy') /* global afterEach, beforeEach, describe, it */ -describe('ncp broken symlink', function () { - const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'ncp-broken-symlinks') +describe('copy() - broken symlink', () => { + const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-broken-symlinks') const src = path.join(TEST_DIR, 'src') const out = path.join(TEST_DIR, 'out') @@ -24,16 +24,16 @@ describe('ncp broken symlink', function () { afterEach(done => fse.remove(TEST_DIR, done)) it('should copy broken symlinks by default', done => { - ncp(src, out, err => { - if (err) return done(err) + copy(src, out, err => { + assert.ifError(err) assert.equal(fs.readlinkSync(path.join(out, 'broken-symlink')), path.join(src, 'does-not-exist')) done() }) }) - it('should return an error when dereference=true', done => { - ncp(src, out, {dereference: true}, err => { - assert.equal(err.code, 'ENOENT') + it('should throw an error when dereference=true', done => { + copy(src, out, {dereference: true}, err => { + assert.strictEqual(err.code, 'ENOENT') done() }) }) diff --git a/lib/copy/__tests__/async/copy-gh-89.test.js b/lib/copy/__tests__/copy-gh-89.test.js similarity index 96% rename from lib/copy/__tests__/async/copy-gh-89.test.js rename to lib/copy/__tests__/copy-gh-89.test.js index 4cfe191c..fb55c2e6 100644 --- a/lib/copy/__tests__/async/copy-gh-89.test.js +++ b/lib/copy/__tests__/copy-gh-89.test.js @@ -22,7 +22,7 @@ describe('copy / gh #89', () => { fse.remove(TEST_DIR, done) }) - it('should...', done => { + it('should copy successfully', done => { const A = path.join(TEST_DIR, 'A') const B = path.join(TEST_DIR, 'B') fs.mkdirSync(A) diff --git a/lib/copy/__tests__/copy-permissions.test.js b/lib/copy/__tests__/copy-permissions.test.js index ccc89cd2..199766af 100644 --- a/lib/copy/__tests__/copy-permissions.test.js +++ b/lib/copy/__tests__/copy-permissions.test.js @@ -12,7 +12,7 @@ const o777 = parseInt('777', 8) const o666 = parseInt('666', 8) const o444 = parseInt('444', 8) -describe('copy', () => { +describe('+ copy() - permissions', () => { let TEST_DIR beforeEach(done => { @@ -89,10 +89,10 @@ describe('copy', () => { const newf2stats = fs.lstatSync(path.join(permDir, 'dest/somedir/f2.bin')) const newd2stats = fs.lstatSync(path.join(permDir, 'dest/crazydir')) - assert.strictEqual(newf1stats.mode, f1stats.mode) - assert.strictEqual(newd1stats.mode, d1stats.mode) - assert.strictEqual(newf2stats.mode, f2stats.mode) - assert.strictEqual(newd2stats.mode, d2stats.mode) + assert.strictEqual(newf1stats.mode, f1stats.mode, 'f1 mode') + assert.strictEqual(newd1stats.mode, d1stats.mode, 'd1 mode') + assert.strictEqual(newf2stats.mode, f2stats.mode, 'f2 mode') + assert.strictEqual(newd2stats.mode, d2stats.mode, 'd2 mode') assert.strictEqual(newf1stats.gid, f1stats.gid) assert.strictEqual(newd1stats.gid, d1stats.gid) diff --git a/lib/copy/__tests__/copy-preserve-time.test.js b/lib/copy/__tests__/copy-preserve-time.test.js index 1c9fcb0d..6bbf627e 100644 --- a/lib/copy/__tests__/copy-preserve-time.test.js +++ b/lib/copy/__tests__/copy-preserve-time.test.js @@ -9,7 +9,7 @@ const assert = require('assert') /* global beforeEach, describe, it */ -describe('copy', () => { +describe('+ copy() - preserve time', () => { let TEST_DIR beforeEach(done => { @@ -21,7 +21,7 @@ describe('copy', () => { const SRC_FIXTURES_DIR = path.join(__dirname, '/fixtures') const FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')] - describe('> when modified option is turned off', () => { + describe('>> when modified option is turned off', () => { it('should have different timestamps on copy', done => { const from = path.join(SRC_FIXTURES_DIR) const to = path.join(TEST_DIR) @@ -33,7 +33,7 @@ describe('copy', () => { }) }) - describe('> when modified option is turned on', () => { + describe('>> when modified option is turned on', () => { it('should have the same timestamps on copy', done => { const from = path.join(SRC_FIXTURES_DIR) const to = path.join(TEST_DIR) 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..9b2586c0 --- /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 not copy and return', 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.ifError(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 not copy and return', 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.ifError(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..6fc138a4 --- /dev/null +++ b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js @@ -0,0 +1,431 @@ +'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 src has contents + assert(srclen > 2) + fs.copy(src, dest, err => { + assert.ifError(err) + + const destlen = klawSync(dest).length + + // assert src and dest length are the same + assert.strictEqual(destlen, srclen, 'src and dest length should be equal') + + 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 directory '${src}' into itself '${dest}'`) + done() + }) +} + +describe('+ copySync() - prevent copying into itself', () => { + let TEST_DIR, src, dest + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-prevent-infinite-recursion') + src = path.join(TEST_DIR, 'src') + fs.mkdirsSync(src) + + // ensureFileSync creates required parent dirs for us:) + FILES.forEach(f => fs.ensureFileSync(path.join(src, f))) + + fs.writeFileSync(path.join(src, FILES[0]), dat0) + fs.writeFileSync(path.join(src, FILES[1]), dat1) + fs.writeFileSync(path.join(src, FILES[2]), dat2) + fs.writeFileSync(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 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 => { + dest = path.join(TEST_DIR, 'src_dest') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'src-dest'`, done => { + dest = path.join(TEST_DIR, 'src-dest') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'dest_src'`, done => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + dest = path.join(TEST_DIR, 'srcsrc', 'dest') + return testSuccess(src, dest, done) + }) + + it(`should copy the directory successfully when dest is 'dest/src'`, done => { + 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 => { + 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 throw error when dest is 'src/dest'`, done => { + dest = path.join(TEST_DIR, 'src', 'dest') + return testError(src, dest, done) + }) + + it(`should throw error when dest is 'src/src_dest'`, done => { + dest = path.join(TEST_DIR, 'src', 'src_dest') + return testError(src, dest, done) + }) + + it(`should throw error when dest is 'src/dest_src'`, done => { + dest = path.join(TEST_DIR, 'src', 'dest_src') + return testError(src, dest, done) + }) + + it(`should throw error when dest is 'src/dest/src'`, done => { + 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 throw an error when resolved dest path is a subdir of src', done => { + 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')) + + // make symlink that points to a subdir in src + fs.symlinkSync(resolvedDestPath, destLink, 'dir') + + fs.copy(src, destLink, err => { + assert.strictEqual(err.message, `Cannot copy directory '${src}' into itself '${resolvedDestPath}'`) + 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 + + 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 not copy and return when resolved src path points to dest', done => { + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'dir') + + dest = path.join(TEST_DIR, 'src') + + fs.copy(srcLink, dest, err => { + assert.ifError(err) + // assert source not affected + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, src) + done() + }) + }) + + it('should throw an error when dest is a subdir of resolved src path', done => { + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'dir') + + dest = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest') + + fs.copy(srcLink, dest, err => { + assert.strictEqual(err.message, `Cannot copy directory '${src}' into itself '${dest}'`) + // assert source not affecte + const link = fs.readlinkSync(srcLink) + assert.strictEqual(link, src) + done() + }) + }) + + it('should copy the directory successfully when resolved src path is a subdir of dest', done => { + const srcLink = path.join(TEST_DIR, 'src_symlink') + const resolvedSrcPath = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src') + fs.copySync(src, resolvedSrcPath) + + // make symlink that points to a subdir in dest + fs.symlinkSync(resolvedSrcPath, srcLink, 'dir') + + dest = path.join(TEST_DIR, 'dest') + + const srclen = klawSync(resolvedSrcPath).length + assert(srclen > 2) + const destlenBefore = klawSync(dest).length + + fs.copy(srcLink, dest, 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() + }) + }) + + 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') + + 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') + + dest = path.join(TEST_DIR, 'src_src', 'dest') + testSuccess(srcLink, dest, () => { + const link = fs.readlinkSync(dest) + assert.strictEqual(link, src) + done() + }) + }) + }) + + describe('>> when dest is a symlink', () => { + it('should not copy and 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 not copy and throw 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.strictEqual(err.message, `Cannot copy directory '${src}' into itself '${resolvedDestPath}'`) + done() + }) + }) + + it('should copy the directory correctly 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') + // put some stuff in resolved src path + fs.copySync(src, srcInDest) + + fs.symlinkSync(srcInDest, srcLink, 'dir') + + dest = path.join(TEST_DIR, 'dest') + + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(dest, destLink, 'dir') + + const srclen = klawSync(srcInDest).length + assert(srclen > 2) + const destlenBefore = klawSync(dest).length + + fs.copy(srcLink, 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() + }) + }) + }) + }) +}) diff --git a/lib/copy/__tests__/ncp/symlink.test.js b/lib/copy/__tests__/copy-symlink.test.js similarity index 82% rename from lib/copy/__tests__/ncp/symlink.test.js rename to lib/copy/__tests__/copy-symlink.test.js index 1b8816b6..cf148dbf 100644 --- a/lib/copy/__tests__/ncp/symlink.test.js +++ b/lib/copy/__tests__/copy-symlink.test.js @@ -3,14 +3,14 @@ const fs = require('fs') const os = require('os') const fse = require(process.cwd()) -const ncp = require('../../ncp') const path = require('path') const assert = require('assert') +const copy = require('../copy') /* global afterEach, beforeEach, describe, it */ -describe('ncp / symlink', () => { - const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'ncp-symlinks') +describe('copy() - symlink', () => { + const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-symlinks') const src = path.join(TEST_DIR, 'src') const out = path.join(TEST_DIR, 'out') @@ -21,21 +21,22 @@ describe('ncp / symlink', () => { }) }) - afterEach(done => fse.remove(TEST_DIR, done)) + afterEach(done => { + fse.remove(TEST_DIR, done) + }) - it('copies symlinks by default', done => { - ncp(src, out, err => { + it('should copy symlinks by default', done => { + copy(src, out, err => { assert.ifError(err) assert.equal(fs.readlinkSync(path.join(out, 'file-symlink')), path.join(src, 'foo')) assert.equal(fs.readlinkSync(path.join(out, 'dir-symlink')), path.join(src, 'dir')) - done() }) }) - it('copies file contents when dereference=true', done => { - ncp(src, out, {dereference: true}, err => { + it('should copy file contents when dereference=true', done => { + copy(src, out, {dereference: true}, err => { assert.ifError(err) const fileSymlinkPath = path.join(out, 'file-symlink') @@ -45,7 +46,6 @@ describe('ncp / symlink', () => { const dirSymlinkPath = path.join(out, 'dir-symlink') assert.ok(fs.lstatSync(dirSymlinkPath).isDirectory()) assert.deepEqual(fs.readdirSync(dirSymlinkPath), ['bar']) - done() }) }) diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js index 6ed64ff8..fa4dfce7 100644 --- a/lib/copy/__tests__/copy.test.js +++ b/lib/copy/__tests__/copy.test.js @@ -69,7 +69,7 @@ describe('fs-extra', () => { }) }) - describe('> when the destination dir does not exist', () => { + describe('>> when the destination dir does not exist', () => { it('should create the destination directory and copy the file', done => { const src = path.join(TEST_DIR, 'file.txt') const dest = path.join(TEST_DIR, 'this/path/does/not/exist/copied.txt') @@ -87,7 +87,7 @@ describe('fs-extra', () => { }) describe('> when the source is a directory', () => { - describe('> when the source directory does not exist', () => { + describe('>> when the source directory does not exist', () => { it('should return an error', done => { const ts = path.join(TEST_DIR, 'this_dir_does_not_exist') const td = path.join(TEST_DIR, 'this_dir_really_does_not_matter') @@ -135,7 +135,7 @@ describe('fs-extra', () => { }) }) - describe('> when the destination dir does not exist', () => { + describe('>> when the destination dir does not exist', () => { it('should create the destination directory and copy the file', done => { const src = path.join(TEST_DIR, 'data/') fse.mkdirsSync(src) @@ -159,6 +159,7 @@ describe('fs-extra', () => { }) }) + /* It is redundant. See above, we already tested for this case. describe('> when src dir does not exist', () => { it('should return an error', done => { fse.copy('/does/not/exist', '/something/else', err => { @@ -167,6 +168,7 @@ describe('fs-extra', () => { }) }) }) + */ }) describe('> when filter is used', () => { diff --git a/lib/copy/__tests__/ncp/README.md b/lib/copy/__tests__/ncp/README.md deleted file mode 100644 index 7b6c04e6..00000000 --- a/lib/copy/__tests__/ncp/README.md +++ /dev/null @@ -1 +0,0 @@ -These tests came from: https://github.com/AvianFlu/ncp/tree/v1.0.1/test \ No newline at end of file diff --git a/lib/copy/__tests__/ncp/fixtures/modified-files/out/a b/lib/copy/__tests__/ncp/fixtures/modified-files/out/a deleted file mode 100644 index d606037c..00000000 --- a/lib/copy/__tests__/ncp/fixtures/modified-files/out/a +++ /dev/null @@ -1 +0,0 @@ -test2 \ No newline at end of file diff --git a/lib/copy/__tests__/ncp/fixtures/modified-files/src/a b/lib/copy/__tests__/ncp/fixtures/modified-files/src/a deleted file mode 100644 index 29f446af..00000000 --- a/lib/copy/__tests__/ncp/fixtures/modified-files/src/a +++ /dev/null @@ -1 +0,0 @@ -test3 \ No newline at end of file diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/a b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/a deleted file mode 100644 index 802992c4..00000000 --- a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/a +++ /dev/null @@ -1 +0,0 @@ -Hello world diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/b b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/b deleted file mode 100644 index 9f6bb185..00000000 --- a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/b +++ /dev/null @@ -1 +0,0 @@ -Hello ncp diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/c b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/c deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/d b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/d deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/e b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/e deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/f b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/f deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/sub/a b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/sub/a deleted file mode 100644 index cf291b5e..00000000 --- a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/sub/a +++ /dev/null @@ -1 +0,0 @@ -Hello nodejitsu diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/sub/b b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/out/sub/b deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/a b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/a deleted file mode 100644 index 802992c4..00000000 --- a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/a +++ /dev/null @@ -1 +0,0 @@ -Hello world diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/b b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/b deleted file mode 100644 index 9f6bb185..00000000 --- a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/b +++ /dev/null @@ -1 +0,0 @@ -Hello ncp diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/c b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/c deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/d b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/d deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/e b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/e deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/f b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/f deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/sub/a b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/sub/a deleted file mode 100644 index cf291b5e..00000000 --- a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/sub/a +++ /dev/null @@ -1 +0,0 @@ -Hello nodejitsu diff --git a/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/sub/b b/lib/copy/__tests__/ncp/fixtures/regular-fixtures/src/sub/b deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/copy/__tests__/ncp/ncp-error-perm.test.js b/lib/copy/__tests__/ncp/ncp-error-perm.test.js deleted file mode 100644 index 451bbdf7..00000000 --- a/lib/copy/__tests__/ncp/ncp-error-perm.test.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict' - -// file in reference: https://github.com/jprichardson/node-fs-extra/issues/56 - -const fs = require('fs') -const os = require('os') -const fse = require(process.cwd()) -const ncp = require('../../ncp') -const path = require('path') -const assert = require('assert') - -/* global afterEach, beforeEach, describe, it */ - -// skip test for windows -// eslint-disable globalReturn */ -// if (os.platform().indexOf('win') === 0) return -// eslint-enable globalReturn */ - -describe('ncp / error / dest-permission', () => { - const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'ncp-error-dest-perm') - const src = path.join(TEST_DIR, 'src') - const dest = path.join(TEST_DIR, 'dest') - - if (os.platform().indexOf('win') === 0) return - - beforeEach(done => { - fse.emptyDir(TEST_DIR, err => { - assert.ifError(err) - done() - }) - }) - - afterEach(done => fse.remove(TEST_DIR, done)) - - it('should return an error', done => { - const someFile = path.join(src, 'some-file') - fse.outputFileSync(someFile, 'hello') - - fse.mkdirsSync(dest) - fs.chmodSync(dest, parseInt('444', 8)) - - const subdest = path.join(dest, 'another-dir') - - ncp(src, subdest, err => { - assert(err) - assert.equal(err.code, 'EACCES') - done() - }) - }) -}) diff --git a/lib/copy/__tests__/ncp/ncp.test.js b/lib/copy/__tests__/ncp/ncp.test.js deleted file mode 100644 index 23ad3fcc..00000000 --- a/lib/copy/__tests__/ncp/ncp.test.js +++ /dev/null @@ -1,193 +0,0 @@ -'use strict' - -const fs = require('fs') -const ncp = require('../../ncp') -const path = require('path') -const rimraf = require('rimraf') -const assert = require('assert') -const readDirFiles = require('read-dir-files').read // temporary, will remove - -/* eslint-env mocha */ - -const fixturesDir = path.join(__dirname, 'fixtures') - -describe('ncp', () => { - describe('regular files and directories', () => { - const fixtures = path.join(fixturesDir, 'regular-fixtures') - const src = path.join(fixtures, 'src') - const out = path.join(fixtures, 'out') - - before(cb => rimraf(out, () => ncp(src, out, cb))) - - describe('when copying a directory of files', () => { - it('files are copied correctly', cb => { - readDirFiles(src, 'utf8', (srcErr, srcFiles) => { - readDirFiles(out, 'utf8', (outErr, outFiles) => { - assert.ifError(srcErr) - assert.deepEqual(srcFiles, outFiles) - cb() - }) - }) - }) - }) - - describe('when copying files using filter', () => { - before(cb => { - const filter = name => name.substr(name.length - 1) !== 'a' - - rimraf(out, () => ncp(src, out, { filter }, cb)) - }) - - it('files are copied correctly', cb => { - readDirFiles(src, 'utf8', (srcErr, srcFiles) => { - function filter (files) { - for (let fileName in files) { - const curFile = files[fileName] - if (curFile instanceof Object) { - return filter(curFile) - } - - if (fileName.substr(fileName.length - 1) === 'a') { - delete files[fileName] - } - } - } - filter(srcFiles) - readDirFiles(out, 'utf8', (outErr, outFiles) => { - assert.ifError(outErr) - assert.deepEqual(srcFiles, outFiles) - cb() - }) - }) - }) - }) - - describe('when using overwrite=true', () => { - before(function () { - this.originalCreateReadStream = fs.createReadStream - }) - - after(function () { - fs.createReadStream = this.originalCreateReadStream - }) - - it('the copy is complete after callback', done => { - ncp(src, out, {overwrite: true}, err => { - fs.createReadStream = () => done(new Error('createReadStream after callback')) - - assert.ifError(err) - process.nextTick(done) - }) - }) - }) - - describe('when using overwrite=false', () => { - beforeEach(done => rimraf(out, done)) - - it('works', cb => { - ncp(src, out, {overwrite: false}, err => { - assert.ifError(err) - cb() - }) - }) - - it('should not error if files exist', cb => { - ncp(src, out, () => { - ncp(src, out, {overwrite: false}, err => { - assert.ifError(err) - cb() - }) - }) - }) - - it('should error if errorOnExist and file exists', cb => { - ncp(src, out, () => { - ncp(src, out, { - overwrite: false, - errorOnExist: true - }, err => { - assert(err) - cb() - }) - }) - }) - }) - - describe('clobber', () => { - beforeEach(done => rimraf(out, done)) - - it('is an alias for overwrite', cb => { - ncp(src, out, () => { - ncp(src, out, { - clobber: false, - errorOnExist: true - }, err => { - assert(err) - cb() - }) - }) - }) - }) - - describe('when using transform', () => { - it('file descriptors are passed correctly', cb => { - ncp(src, out, { - transform: (read, write, file) => { - assert.notEqual(file.name, undefined) - assert.strictEqual(typeof file.mode, 'number') - read.pipe(write) - } - }, cb) - }) - }) - }) - - // see https://github.com/AvianFlu/ncp/issues/71 - describe('Issue 71: Odd Async Behaviors', cb => { - const fixtures = path.join(__dirname, 'fixtures', 'regular-fixtures') - const src = path.join(fixtures, 'src') - const out = path.join(fixtures, 'out') - - let totalCallbacks = 0 - - function copyAssertAndCount (callback) { - // rimraf(out, function() { - ncp(src, out, err => { - assert(!err) - totalCallbacks += 1 - readDirFiles(src, 'utf8', (srcErr, srcFiles) => { - readDirFiles(out, 'utf8', (outErr, outFiles) => { - assert.ifError(srcErr) - assert.deepEqual(srcFiles, outFiles) - callback() - }) - }) - }) - // }) - } - - describe('when copying a directory of files without cleaning the destination', () => { - it('callback fires once per run and directories are equal', done => { - const expected = 10 - let count = 10 - - function next () { - if (count > 0) { - setTimeout(() => { - copyAssertAndCount(() => { - count -= 1 - next() - }) - }, 100) - } else { - // console.log('Total callback count is', totalCallbacks) - assert.equal(totalCallbacks, expected) - done() - } - } - - next() - }) - }) - }) -}) diff --git a/lib/copy/copy.js b/lib/copy/copy.js index d66c8981..7f561e29 100644 --- a/lib/copy/copy.js +++ b/lib/copy/copy.js @@ -2,20 +2,33 @@ const fs = require('graceful-fs') const path = require('path') -const ncp = require('./ncp') -const mkdir = require('../mkdirs') +const mkdirs = require('../mkdirs').mkdirs +const utimes = require('../util/utimes').utimesMillis -function copy (src, dest, options, callback) { - if (typeof options === 'function' && !callback) { - callback = options +const DEST_NOENT = -1 +const DEST_EXISTS = 1 + +function copy (src, dest, options, cb) { + if (typeof options === 'function' && !cb) { + cb = options options = {} } else if (typeof options === 'function' || options instanceof RegExp) { options = {filter: options} } - callback = callback || function () {} + + cb = cb || function () {} options = options || {} - // Warn about using preserveTimestamps on 32-bit node: + // default to true for now + options.clobber = 'clobber' in options ? !!options.clobber : true + // overwrite falls back to clobber + options.overwrite = 'overwrite' in options ? !!options.overwrite : options.clobber + options.dereference = 'dereference' in options ? !!options.dereference : false + options.preserveTimestamps = 'preserveTimestamps' in options ? !!options.preserveTimestamps : false + + options.filter = options.filter || function () { return true } + + // Warn about using preserveTimestamps on 32-bit node if (options.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`) @@ -23,30 +36,179 @@ function copy (src, dest, options, callback) { // 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) - } else { - dir = path.dirname(dest) + src = path.resolve(basePath, src) + dest = path.resolve(basePath, dest) + if (src === dest) return cb(new Error('Source and destination must not be the same.')) + + if (options.filter) { + if (options.filter instanceof RegExp) { + console.warn('Warning: fs-extra: Passing a RegExp filter is deprecated, use a function') + if (!options.filter.test(src)) return cb() + } else if (typeof options.filter === 'function') { + if (!options.filter(src, dest)) return cb() + } + } + + const destParent = path.dirname(dest) + mkdirs(destParent, err => { + if (err && err.code !== 'EEXIST') return cb(err) + + let stat = options.dereference ? fs.stat : fs.lstat + stat(src, (err, srcStat) => { + if (err) return cb(err) + + if (srcStat.isDirectory()) { + return onDir(src, srcStat, dest, options, cb) + } else if (srcStat.isFile() || srcStat.isCharacterDevice() || srcStat.isBlockDevice()) { + return onFile(src, srcStat, dest, options, cb) + } else if (srcStat.isSymbolicLink() && !options.dereference) { + return onLink(src, dest, options, cb) + } + }) + }) +} + +function checkDest (dest, cb) { + fs.readlink(dest, (err, resolvedDestPath) => { + if (err) { + if (err.code === 'ENOENT') return cb(null, DEST_NOENT) + // dest exists but is not a link + else if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return cb(null, DEST_EXISTS) + else return cb(err) + } else { // dest exists and is a link + return cb(null, resolvedDestPath) } + }) +} + +function onFile (src, srcStat, dest, options, cb) { + checkDest(dest, (err, res) => { + if (err) return cb(err) + if (res === DEST_NOENT) { + return cpFile(src, srcStat, dest, options, cb) + } else if (res === DEST_EXISTS) { + if (options.overwrite) { + fs.unlink(dest, err => { + if (err) return cb(err) + return cpFile(src, srcStat, dest, options, cb) + }) + } else if (options.errorOnExist) { + return cb(new Error(dest + ' already exists')) + } else return cb() + } else return cb() + }) +} + +function cpFile (src, srcStat, dest, options, cb) { + const rs = fs.createReadStream(src) + const ws = fs.createWriteStream(dest, { mode: srcStat.mode }) - fs.exists(dir, dirExists => { - 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 (options.preserveTimestamps) { + return utimes(dest, srcStat.atime, srcStat.mtime, cb) + } else return cb() + }) + }) +} + +function onDir (src, srcStat, dest, options, cb) { + checkDest(dest, (err, res) => { + if (err) return cb(err) + if (res === DEST_NOENT) { + // if dest is a subdir of src, prevent copying into itself + if (isSrcSubdir(src, dest)) return cb(new Error(`Cannot copy directory '${src}' into itself '${dest}'`)) + 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, options, cb) + }) }) + } else if (res === DEST_EXISTS) { + if (isSrcSubdir(src, dest)) return cb(new Error(`Cannot copy directory '${src}' into itself '${dest}'`)) + return cpDir(src, dest, options, cb) + } else if (res && typeof res !== 'number') { // dest exists and is a link + if (isSrcSubdir(src, res)) return cb(new Error(`Cannot copy directory '${src}' into itself '${res}'`)) + if (src === res) return cb() + return cpDir(src, dest, options, cb) + } else return cb() + }) +} + +function cpDir (src, dest, options, cb) { + fs.readdir(src, (err, items) => { + if (err) return cb(err) + Promise.all(items.map(item => { + return new Promise((resolve, reject) => { + copy(path.join(src, item), path.join(dest, item), options, err => { + if (err) reject(err) + else resolve() + }) + }) + })).then(() => { + return cb() + }).catch(err => { + return cb(err) + }) + }) +} + +function onLink (src, dest, options, cb) { + fs.readlink(src, (err, resolvedSrcPath) => { + if (err) return cb(err) + + if (options.dereference) { + resolvedSrcPath = path.resolve(process.cwd(), resolvedSrcPath) + } + + checkDest(dest, (err, resolvedDestPath) => { + if (err) return cb(err) + + if (resolvedDestPath === DEST_NOENT) { + // if dest is a subdir of resolved src path, prevent copying into itself + if (isSrcSubdir(resolvedSrcPath, dest)) { + return cb(new Error(`Cannot copy directory '${resolvedSrcPath}' into itself '${dest}'`)) + } + return fs.symlink(resolvedSrcPath, dest, cb) + } else if (resolvedDestPath === DEST_EXISTS) { // dest exists but is not a link + if (isSrcSubdir(resolvedSrcPath, dest)) { + return cb(new Error(`Cannot copy directory '${resolvedSrcPath}' into itself '${dest}'`)) + } + // if src points to dest + if (resolvedSrcPath === dest) return cb() + return copy(resolvedSrcPath, dest, options, cb) + } else if (resolvedDestPath && typeof resolvedDestPath !== 'number') { // dest is a link + if (options.dereference) { + resolvedDestPath = path.resolve(process.cwd(), resolvedDestPath) + } + // if resolved dest path is a subdir of resolved src path, prevent copying into itself + if (isSrcSubdir(resolvedSrcPath, resolvedDestPath)) { + return cb(new Error(`Cannot copy directory '${resolvedSrcPath}' into itself '${resolvedDestPath}'`)) + } + if (resolvedSrcPath === resolvedDestPath) return cb() + return copy(resolvedSrcPath, dest, options, cb) + } else return cb() }) }) } +// 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) { + try { + return src !== dest && + dest.indexOf(src) > -1 && + dest.split(path.dirname(src) + path.sep)[1].split(path.sep)[0] === path.basename(src) + } catch (e) { + 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/__tests__/move.test.js b/lib/move/__tests__/move.test.js index fc467b96..1d0e51d0 100644 --- a/lib/move/__tests__/move.test.js +++ b/lib/move/__tests__/move.test.js @@ -1,20 +1,18 @@ -'use strict' - -const fs = require('graceful-fs') -const os = require('os') -const fse = require(process.cwd()) -const path = require('path') -const assert = require('assert') -const rimraf = require('rimraf') +var assert = require('assert') +var os = require('os') +var path = require('path') +var rimraf = require('rimraf') +var fs = require('graceful-fs') +var fse = require(process.cwd()) /* global afterEach, beforeEach, describe, it */ function createAsyncErrFn (errCode) { - const fn = function () { + var fn = function () { fn.callCount++ - const callback = arguments[arguments.length - 1] - setTimeout(() => { - const err = new Error() + var callback = arguments[arguments.length - 1] + setTimeout(function () { + var err = new Error() err.code = errCode callback(err) }, 10) @@ -23,8 +21,8 @@ function createAsyncErrFn (errCode) { return fn } -const originalRename = fs.rename -const originalLink = fs.link +var originalRename = fs.rename +var originalLink = fs.link function setUpMockFs (errCode) { fs.rename = createAsyncErrFn(errCode) @@ -36,10 +34,10 @@ function tearDownMockFs () { fs.link = originalLink } -describe('move', () => { - let TEST_DIR +describe('move', function () { + var TEST_DIR - beforeEach(() => { + beforeEach(function () { TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'move') fse.emptyDirSync(TEST_DIR) @@ -52,16 +50,18 @@ describe('move', () => { fs.writeFileSync(path.join(TEST_DIR, 'a-folder/another-folder/file3'), 'knuckles\n') }) - afterEach(done => rimraf(TEST_DIR, done)) + afterEach(function (done) { + rimraf(TEST_DIR, done) + }) - it('should rename a file on the same device', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-file-dest` + it('should rename a file on the same device', function (done) { + var src = TEST_DIR + '/a-file' + var dest = TEST_DIR + '/a-file-dest' - fse.move(src, dest, err => { + fse.move(src, dest, function (err) { assert.ifError(err) - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ + fs.readFile(dest, 'utf8', function (err, contents) { + var expected = /^sonic the hedgehog\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) done() @@ -69,43 +69,43 @@ describe('move', () => { }) }) - it('should not overwrite the destination by default', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-folder/another-file` + it('should not overwrite the destination by default', function (done) { + var src = TEST_DIR + '/a-file' + var dest = TEST_DIR + '/a-folder/another-file' // verify file exists already assert(fs.existsSync(dest)) - fse.move(src, dest, err => { + fse.move(src, dest, function (err) { assert.ok(err && err.code === 'EEXIST', 'throw EEXIST') done() }) }) - it('should not overwrite if overwrite = false', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-folder/another-file` + it('should not overwrite if overwrite = false', function (done) { + var src = TEST_DIR + '/a-file' + var dest = TEST_DIR + '/a-folder/another-file' // verify file exists already assert(fs.existsSync(dest)) - fse.move(src, dest, {overwrite: false}, err => { + fse.move(src, dest, {overwrite: false}, function (err) { assert.ok(err && err.code === 'EEXIST', 'throw EEXIST') done() }) }) - it('should overwrite file if overwrite = true', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-folder/another-file` + it('should overwrite file if overwrite = true', function (done) { + var src = TEST_DIR + '/a-file' + var dest = TEST_DIR + '/a-folder/another-file' // verify file exists already assert(fs.existsSync(dest)) - fse.move(src, dest, {overwrite: true}, err => { + fse.move(src, dest, {overwrite: true}, function (err) { assert.ifError(err) - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ + fs.readFile(dest, 'utf8', function (err, contents) { + var expected = /^sonic the hedgehog\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) done() @@ -121,23 +121,23 @@ describe('move', () => { this.timeout(90000) // Create src - const src = path.join(TEST_DIR, 'src') + var src = path.join(TEST_DIR, 'src') fse.ensureDirSync(src) fse.mkdirsSync(path.join(src, 'some-folder')) fs.writeFileSync(path.join(src, 'some-file'), 'hi') - const dest = path.join(TEST_DIR, 'a-folder') + var dest = path.join(TEST_DIR, 'a-folder') // verify dest has stuff in it - const paths = fs.readdirSync(dest) + var paths = fs.readdirSync(dest) assert(paths.indexOf('another-file') >= 0) assert(paths.indexOf('another-folder') >= 0) - fse.move(src, dest, {overwrite: true}, err => { + fse.move(src, dest, {overwrite: true}, function (err) { assert.ifError(err) // verify dest does not have old stuff - const paths = fs.readdirSync(dest) + var paths = fs.readdirSync(dest) assert.strictEqual(paths.indexOf('another-file'), -1) assert.strictEqual(paths.indexOf('another-folder'), -1) @@ -149,30 +149,30 @@ describe('move', () => { }) }) - it('should not create directory structure if mkdirp is false', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/does/not/exist/a-file-dest` + it('should not create directory structure if mkdirp is false', function (done) { + var src = TEST_DIR + '/a-file' + var dest = TEST_DIR + '/does/not/exist/a-file-dest' // verify dest directory does not exist assert(!fs.existsSync(path.dirname(dest))) - fse.move(src, dest, {mkdirp: false}, err => { + fse.move(src, dest, {mkdirp: false}, function (err) { assert.strictEqual(err.code, 'ENOENT') done() }) }) - it('should create directory structure by default', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/does/not/exist/a-file-dest` + it('should create directory structure by default', function (done) { + var src = TEST_DIR + '/a-file' + var dest = TEST_DIR + '/does/not/exist/a-file-dest' // verify dest directory does not exist assert(!fs.existsSync(path.dirname(dest))) - fse.move(src, dest, err => { + fse.move(src, dest, function (err) { assert.ifError(err) - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ + fs.readFile(dest, 'utf8', function (err, contents) { + var expected = /^sonic the hedgehog\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) done() @@ -180,18 +180,18 @@ describe('move', () => { }) }) - it('should work across devices', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-file-dest` + it('should work across devices', function (done) { + var src = TEST_DIR + '/a-file' + var dest = TEST_DIR + '/a-file-dest' setUpMockFs('EXDEV') - fse.move(src, dest, err => { + fse.move(src, dest, function (err) { assert.ifError(err) assert.strictEqual(fs.link.callCount, 1) - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ + fs.readFile(dest, 'utf8', function (err, contents) { + var expected = /^sonic the hedgehog\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) @@ -201,17 +201,17 @@ describe('move', () => { }) }) - it('should move folders', done => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` + it('should move folders', function (done) { + var src = TEST_DIR + '/a-folder' + var dest = TEST_DIR + '/a-folder-dest' // verify it doesn't exist assert(!fs.existsSync(dest)) - fse.move(src, dest, err => { + fse.move(src, dest, function (err) { assert.ifError(err) - fs.readFile(dest + '/another-file', 'utf8', (err, contents) => { - const expected = /^tails\r?\n$/ + fs.readFile(dest + '/another-file', 'utf8', function (err, contents) { + var expected = /^tails\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) done() @@ -219,18 +219,18 @@ describe('move', () => { }) }) - it('should move folders across devices with EISDIR error', done => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` + it('should move folders across devices with EISDIR error', function (done) { + var src = TEST_DIR + '/a-folder' + var dest = TEST_DIR + '/a-folder-dest' setUpMockFs('EISDIR') - fse.move(src, dest, err => { + fse.move(src, dest, function (err) { assert.ifError(err) assert.strictEqual(fs.link.callCount, 1) - fs.readFile(dest + '/another-folder/file3', 'utf8', (err, contents) => { - const expected = /^knuckles\r?\n$/ + fs.readFile(dest + '/another-folder/file3', 'utf8', function (err, contents) { + var expected = /^knuckles\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) @@ -241,20 +241,20 @@ describe('move', () => { }) }) - it('should overwrite folders across devices', done => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` + it('should overwrite folders across devices', function (done) { + var src = TEST_DIR + '/a-folder' + var dest = TEST_DIR + '/a-folder-dest' fs.mkdirSync(dest) setUpMockFs('EXDEV') - fse.move(src, dest, {overwrite: true}, err => { + fse.move(src, dest, {overwrite: true}, function (err) { assert.ifError(err) assert.strictEqual(fs.rename.callCount, 1) - fs.readFile(dest + '/another-folder/file3', 'utf8', (err, contents) => { - const expected = /^knuckles\r?\n$/ + fs.readFile(dest + '/another-folder/file3', 'utf8', function (err, contents) { + var expected = /^knuckles\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) @@ -265,18 +265,18 @@ describe('move', () => { }) }) - it('should move folders across devices with EXDEV error', done => { - const src = `${TEST_DIR}/a-folder` - const dest = `${TEST_DIR}/a-folder-dest` + it('should move folders across devices with EXDEV error', function (done) { + var src = TEST_DIR + '/a-folder' + var dest = TEST_DIR + '/a-folder-dest' setUpMockFs('EXDEV') - fse.move(src, dest, err => { + fse.move(src, dest, function (err) { assert.ifError(err) assert.strictEqual(fs.link.callCount, 1) - fs.readFile(dest + '/another-folder/file3', 'utf8', (err, contents) => { - const expected = /^knuckles\r?\n$/ + fs.readFile(dest + '/another-folder/file3', 'utf8', function (err, contents) { + var expected = /^knuckles\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) @@ -287,18 +287,18 @@ describe('move', () => { }) }) - describe('clobber', () => { - it('should be an alias for overwrite', done => { - const src = `${TEST_DIR}/a-file` - const dest = `${TEST_DIR}/a-folder/another-file` + describe('clobber', function () { + it('should be an alias for overwrite', function (done) { + var src = TEST_DIR + '/a-file' + var dest = TEST_DIR + '/a-folder/another-file' // verify file exists already assert(fs.existsSync(dest)) - fse.move(src, dest, {overwrite: true}, err => { + fse.move(src, dest, {overwrite: true}, function (err) { assert.ifError(err) - fs.readFile(dest, 'utf8', (err, contents) => { - const expected = /^sonic the hedgehog\r?\n$/ + fs.readFile(dest, 'utf8', function (err, contents) { + var expected = /^sonic the hedgehog\r?\n$/ assert.ifError(err) assert.ok(contents.match(expected), `${contents} match ${expected}`) done() @@ -307,16 +307,16 @@ describe('move', () => { }) }) - describe.skip('> when trying to a move a folder into itself', () => { - it('should produce an error', done => { - const SRC_DIR = path.join(TEST_DIR, 'test') - const DEST_DIR = path.join(TEST_DIR, 'test', 'test') + describe('> when trying to a move a folder into itself', function () { + it('should produce an error', function (done) { + var SRC_DIR = path.join(TEST_DIR, 'test') + var DEST_DIR = path.join(TEST_DIR, 'test', 'test') assert(!fs.existsSync(SRC_DIR)) fs.mkdirSync(SRC_DIR) assert(fs.existsSync(SRC_DIR)) - fse.move(SRC_DIR, DEST_DIR, err => { + fse.move(SRC_DIR, DEST_DIR, function (err) { assert(fs.existsSync(SRC_DIR)) assert(err) done() @@ -327,9 +327,9 @@ describe('move', () => { // tested on Linux ubuntu 3.13.0-32-generic #57-Ubuntu SMP i686 i686 GNU/Linux // this won't trigger a bug on Mac OS X Yosimite with a USB drive (/Volumes) // see issue #108 - describe('> when actually trying to a move a folder across devices', () => { - const differentDevice = '/mnt' - let __skipTests = false + describe('> when actually trying to a move a folder across devices', function () { + var differentDevice = '/mnt' + var __skipTests = false // must set this up, if not, exit silently if (!fs.existsSync(differentDevice)) { @@ -345,12 +345,12 @@ describe('move', () => { __skipTests = true } - const _it = __skipTests ? it.skip : it + var _it = __skipTests ? it.skip : it - describe('> just the folder', () => { - _it('should move the folder', done => { - const src = '/mnt/some/weird/dir-really-weird' - const dest = path.join(TEST_DIR, 'device-weird') + describe('> just the folder', function () { + _it('should move the folder', function (done) { + var src = '/mnt/some/weird/dir-really-weird' + var dest = path.join(TEST_DIR, 'device-weird') if (!fs.existsSync(src)) { fse.mkdirpSync(src) @@ -360,7 +360,7 @@ describe('move', () => { assert(fs.lstatSync(src).isDirectory()) - fse.move(src, dest, err => { + fse.move(src, dest, function (err) { assert.ifError(err) assert(fs.existsSync(dest)) // console.log(path.normalize(dest)) diff --git a/lib/move/index.js b/lib/move/index.js index cc4b5935..b8cf77de 100644 --- a/lib/move/index.js +++ b/lib/move/index.js @@ -7,8 +7,8 @@ // this needs a cleanup const fs = require('graceful-fs') -const ncp = require('../copy/ncp') const path = require('path') +const copy = require('../copy').copy const remove = require('../remove').remove const mkdirp = require('../mkdirs').mkdirs @@ -140,14 +140,14 @@ function moveDirAcrossDevice (source, dest, overwrite, callback) { if (overwrite) { remove(dest, err => { if (err) return callback(err) - startNcp() + startCopy() }) } else { - startNcp() + startCopy() } - function startNcp () { - ncp(source, dest, options, err => { + function startCopy () { + copy(source, dest, options, err => { if (err) return callback(err) remove(source, callback) }) diff --git a/package.json b/package.json index 6dae7baa..1fddbad3 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "coveralls": "^2.11.2", "istanbul": "^0.4.5", "klaw": "^1.0.0", + "klaw-sync": "^1.1.2", "minimist": "^1.1.1", "mocha": "^3.1.2", "proxyquire": "^1.7.10",