diff --git a/lib/copy/__tests__/async/copy-gh-89.test.js b/lib/copy/__tests__/async/copy-gh-89.test.js index 4cfe191c..fb55c2e6 100644 --- a/lib/copy/__tests__/async/copy-gh-89.test.js +++ b/lib/copy/__tests__/async/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-broken-symlink.test.js b/lib/copy/__tests__/copy-broken-symlink.test.js new file mode 100644 index 00000000..b598acfc --- /dev/null +++ b/lib/copy/__tests__/copy-broken-symlink.test.js @@ -0,0 +1,61 @@ +'use strict' + +const fs = require('fs') +const os = require('os') +const fse = require(process.cwd()) +const path = require('path') +const assert = require('assert') +const copy = require('../copy') + +/* global afterEach, beforeEach, describe, it */ + +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') + + beforeEach(done => { + fse.emptyDir(TEST_DIR, err => { + assert.ifError(err) + createFixtures(src, done) + }) + }) + + afterEach(done => fse.remove(TEST_DIR, done)) + + it('should copy broken symlinks by default', done => { + 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 throw an error when dereference=true', done => { + copy(src, out, {dereference: true}, err => { + assert.strictEqual(err.code, 'ENOENT') + done() + }) + }) +}) + +function createFixtures (srcDir, callback) { + fs.mkdir(srcDir, err => { + let brokenFile + let brokenFileLink + + if (err) return callback(err) + + try { + brokenFile = path.join(srcDir, 'does-not-exist') + brokenFileLink = path.join(srcDir, 'broken-symlink') + fs.writeFileSync(brokenFile, 'does not matter') + fs.symlinkSync(brokenFile, brokenFileLink, 'file') + } catch (err) { + callback(err) + } + + // break the symlink now + fse.remove(brokenFile, callback) + }) +} 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..ec80fd6c --- /dev/null +++ b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js @@ -0,0 +1,399 @@ +'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.equal(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.equal(o0, dat0, 'file contents matched') + assert.equal(o1, dat1, 'file contents matched') + assert.equal(o2, dat2, 'file contents matched') + assert.equal(o3, dat3, 'file contents matched') + done() + }) +} + +function testError (src, dest, done) { + fs.copy(src, dest, err => { + assert.equal(err.message, `Cannot copy directory '${src}' into itself '${dest}'`) + done() + }) +} + +describe('+ copy() - prevent copying into itself', () => { + let TEST_DIR, src, dest + + beforeEach(done => { + TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-prevent-copying-into-itself') + 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 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.equal(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.equal(srclenAfter, srclenBefore, 'src length should not change') + + const link = fs.readlinkSync(destLink) + assert.equal(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.equal(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.equal(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.equal(o0, dat0, 'files contents matched') + assert.equal(o1, dat1, 'files contents matched') + assert.equal(o2, dat2, 'files contents matched') + assert.equal(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.equal(link, src) + done() + }) + }) + + it('should throw an error when dest not exist and 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.equal(err.message, `Cannot copy directory '${src}' into itself '${dest}'`) + const link = fs.readlinkSync(srcLink) + assert.equal(link, src) + done() + }) + }) + + it('should throw an error when dest exists and 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.mkdirsSync(dest) + + fs.copy(srcLink, dest, err => { + assert.equal(err.message, `Cannot copy directory '${src}' into itself '${dest}'`) + const link = fs.readlinkSync(srcLink) + assert.equal(link, src) + done() + }) + }) + + it('should throw an error when resolved src path is a subdir of dest', done => { + 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.equal(err.message, `'${dest}' already exists and contains '${srcLink}'`) + 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.equal(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, 'srcsrc', 'dest') + testSuccess(srcLink, dest, () => { + const link = fs.readlinkSync(dest) + assert.equal(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.equal(srclenAfter, srclenBefore, 'src length should not change') + const destlenAfter = klawSync(destLink).length + assert.equal(destlenAfter, destlenBefore, 'dest length should not change') + + const srcln = fs.readlinkSync(srcLink) + assert.equal(srcln, src) + const destln = fs.readlinkSync(destLink) + assert.equal(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.equal(err.message, `Cannot copy directory '${src}' into itself '${resolvedDestPath}'`) + done() + }) + }) + + it('should throw an 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') + // 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') + + fs.copy(srcLink, destLink, err => { + assert.equal(err.message, `'${destLink}' already exists and contains '${srcLink}'`) + done() + }) + }) + }) + }) +}) diff --git a/lib/copy/__tests__/copy-symlink.test.js b/lib/copy/__tests__/copy-symlink.test.js new file mode 100644 index 00000000..f263e4b4 --- /dev/null +++ b/lib/copy/__tests__/copy-symlink.test.js @@ -0,0 +1,140 @@ +'use strict' + +const fs = require('fs') +const os = require('os') +const fse = require(process.cwd()) +const path = require('path') +const assert = require('assert') +const copy = require('../copy') + +/* global afterEach, beforeEach, describe, it */ + +describe('copy() - symlink', () => { + const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-symlink') + const src = path.join(TEST_DIR, 'src') + const out = path.join(TEST_DIR, 'out') + + beforeEach(done => { + fse.emptyDir(TEST_DIR, err => { + assert.ifError(err) + createFixtures(src, done) + }) + }) + + afterEach(done => { + fse.remove(TEST_DIR, done) + }) + + 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('should copy file contents when dereference=true and dest not exist', done => { + copy(src, out, {dereference: true}, err => { + assert.ifError(err) + + const fileSymlinkPath = path.join(out, 'file-symlink') + assert.ok(fs.lstatSync(fileSymlinkPath).isFile()) + assert.equal(fs.readFileSync(fileSymlinkPath), 'foo contents') + + const dirSymlinkPath = path.join(out, 'dir-symlink') + assert.ok(fs.lstatSync(dirSymlinkPath).isDirectory()) + assert.deepEqual(fs.readdirSync(dirSymlinkPath), ['bar']) + done() + }) + }) + + it('should copy file contents when dereference=true and dest exists', done => { + const dest = path.join(TEST_DIR, 'dest') + fse.mkdirsSync(dest) + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(dest, destLink, 'dir') + + copy(src, destLink, {dereference: true}, err => { + assert.ifError(err) + + const fileSymlinkPath = path.join(destLink, 'file-symlink') + assert.ok(fs.lstatSync(fileSymlinkPath).isFile()) + assert.equal(fs.readFileSync(fileSymlinkPath), 'foo contents') + + const dirSymlinkPath = path.join(destLink, 'dir-symlink') + assert.ok(fs.lstatSync(dirSymlinkPath).isDirectory()) + assert.deepEqual(fs.readdirSync(dirSymlinkPath), ['bar']) + done() + }) + }) + + it('should copy file contents when dereference=true and src is a link and dest is regular', done => { + const dest = path.join(TEST_DIR, 'dest') + fse.mkdirsSync(dest) + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'dir') + + copy(srcLink, dest, {dereference: true}, err => { + assert.ifError(err) + + const fileSymlinkPath = path.join(dest, 'file-symlink') + assert.ok(fs.lstatSync(fileSymlinkPath).isFile()) + assert.equal(fs.readFileSync(fileSymlinkPath), 'foo contents') + + const dirSymlinkPath = path.join(dest, 'dir-symlink') + assert.ok(fs.lstatSync(dirSymlinkPath).isDirectory()) + assert.deepEqual(fs.readdirSync(dirSymlinkPath), ['bar']) + done() + }) + }) + + it('should copy file contents when dereference=true and src and dest are links', done => { + const dest = path.join(TEST_DIR, 'dest') + fse.mkdirsSync(dest) + const srcLink = path.join(TEST_DIR, 'src_symlink') + fs.symlinkSync(src, srcLink, 'dir') + const destLink = path.join(TEST_DIR, 'dest_symlink') + fs.symlinkSync(dest, destLink, 'dir') + + copy(srcLink, destLink, {dereference: true}, err => { + assert.ifError(err) + + const fileSymlinkPath = path.join(destLink, 'file-symlink') + assert.ok(fs.lstatSync(fileSymlinkPath).isFile()) + assert.equal(fs.readFileSync(fileSymlinkPath), 'foo contents') + + const dirSymlinkPath = path.join(destLink, 'dir-symlink') + assert.ok(fs.lstatSync(dirSymlinkPath).isDirectory()) + assert.deepEqual(fs.readdirSync(dirSymlinkPath), ['bar']) + done() + }) + }) +}) + +function createFixtures (srcDir, callback) { + fs.mkdir(srcDir, err => { + if (err) return callback(err) + + // note: third parameter in symlinkSync is type e.g. 'file' or 'dir' + // https://nodejs.org/api/fs.html#fs_fs_symlink_srcpath_dstpath_type_callback + try { + const fooFile = path.join(srcDir, 'foo') + const fooFileLink = path.join(srcDir, 'file-symlink') + fs.writeFileSync(fooFile, 'foo contents') + fs.symlinkSync(fooFile, fooFileLink, 'file') + + const dir = path.join(srcDir, 'dir') + const dirFile = path.join(dir, 'bar') + const dirLink = path.join(srcDir, 'dir-symlink') + fs.mkdirSync(dir) + fs.writeFileSync(dirFile, 'bar contents') + fs.symlinkSync(dir, dirLink, 'dir') + } catch (err) { + callback(err) + } + + callback() + }) +} diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js index 6ed64ff8..91e17d20 100644 --- a/lib/copy/__tests__/copy.test.js +++ b/lib/copy/__tests__/copy.test.js @@ -39,7 +39,7 @@ describe('fs-extra', () => { let destMd5 = '' fse.copy(fileSrc, fileDest, err => { - assert(!err) + assert.ifError(err) destMd5 = crypto.createHash('md5').update(fs.readFileSync(fileDest)).digest('hex') assert.strictEqual(srcMd5, destMd5) done() @@ -63,13 +63,13 @@ describe('fs-extra', () => { fs.writeFileSync(fileSrc, '') fse.copy(fileSrc, fileDest, err => { - assert(!err) + assert.ifError(err) fs.statSync(fileDest) done() }) }) - 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') @@ -104,14 +104,14 @@ describe('fs-extra', () => { const dest = path.join(TEST_DIR, 'dest') fse.mkdirs(src, err => { - assert(!err) + assert.ifError(err) for (let i = 0; i < FILES; ++i) { fs.writeFileSync(path.join(src, i.toString()), crypto.randomBytes(SIZE)) } const subdir = path.join(src, 'subdir') fse.mkdirs(subdir, err => { - assert(!err) + assert.ifError(err) for (let i = 0; i < FILES; ++i) { fs.writeFileSync(path.join(subdir, i.toString()), crypto.randomBytes(SIZE)) } @@ -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) @@ -158,15 +158,6 @@ describe('fs-extra', () => { }) }) }) - - describe('> when src dir does not exist', () => { - it('should return an error', done => { - fse.copy('/does/not/exist', '/something/else', err => { - assert(err instanceof Error) - done() - }) - }) - }) }) describe('> when filter is used', () => { @@ -177,7 +168,7 @@ describe('fs-extra', () => { const filter = s => s.split('.').pop() !== 'css' fse.copy(srcFile1, destFile1, filter, err => { - assert(!err) + assert.ifError(err) assert(!fs.existsSync(destFile1)) done() }) @@ -190,7 +181,20 @@ describe('fs-extra', () => { const options = { filter: s => /.html$|.css$/i.test(s) } fse.copy(srcFile1, destFile1, options, (err) => { - assert(!err) + assert.ifError(err) + assert(!fs.existsSync(destFile1)) + done() + }) + }) + + it('should not copy and return when nothing matched', done => { + const srcFile1 = path.join(TEST_DIR, '1.jade') + fs.writeFileSync(srcFile1, '') + const destFile1 = path.join(TEST_DIR, 'dest1.jade') + const filter = /.html$|.css$/i + + fse.copy(srcFile1, destFile1, filter, (err) => { + assert.ifError(err) assert(!fs.existsSync(destFile1)) done() }) @@ -216,7 +220,7 @@ describe('fs-extra', () => { } const dest = path.join(TEST_DIR, 'dest') fse.copy(src, dest, filter, err => { - assert(!err) + assert.ifError(err) assert(fs.existsSync(dest)) assert(FILES > 1) @@ -266,7 +270,7 @@ describe('fs-extra', () => { it('should apply filter when it is applied only to dest', done => { const timeCond = new Date().getTime() - const filter = (s, d) => fs.statSync(d).birthtime.getTime() < timeCond + const filter = (s, d) => fs.existsSync(d) && fs.statSync(d).birthtime.getTime() < timeCond const src = path.join(TEST_DIR, 'src') fse.mkdirsSync(src) @@ -279,7 +283,7 @@ describe('fs-extra', () => { fse.mkdirsSync(dest) fse.copy(src, dest, filter, err => { - assert(!err) + assert.ifError(err) assert(!fs.existsSync(path.join(dest, 'subdir'))) done() }) @@ -307,15 +311,15 @@ describe('fs-extra', () => { const destFile3 = path.join(dest, 'dest3.jade') fse.copy(srcFile1, destFile1, filter, err => { - assert(!err) + assert.ifError(err) assert(fs.existsSync(destFile1)) fse.copy(srcFile2, destFile2, filter, err => { - assert(!err) + assert.ifError(err) assert(!fs.existsSync(destFile2)) fse.copy(srcFile3, destFile3, filter, err => { - assert(!err) + assert.ifError(err) assert(fs.existsSync(destFile3)) done() }) @@ -323,6 +327,54 @@ describe('fs-extra', () => { }) }, 1000) }) + + it('should apply filter recursively on failed dirs by default', done => { + const srcDir = path.join(TEST_DIR, 'src') + const srcFile1 = path.join(srcDir, '1.js') + const srcFile2 = path.join(srcDir, '2.css') + const srcFile3 = path.join(srcDir, 'node_modules', '3.css') + fse.outputFileSync(srcFile1, 'file 1 stuff') + fse.outputFileSync(srcFile2, 'file 2 stuff') + fse.outputFileSync(srcFile3, 'file 3 stuff') + const destDir = path.join(TEST_DIR, 'dest') + const destFile1 = path.join(destDir, '1.js') + const destFile2 = path.join(destDir, '2.css') + const destFile3 = path.join(destDir, 'node_modules', '3.css') + + const filter = s => path.extname(s) === '.css' + + fse.copy(srcDir, destDir, filter, err => { + assert(!err) + assert(!fs.existsSync(destFile1)) + assert(fs.existsSync(destFile2)) + assert(fs.existsSync(destFile3)) + done() + }) + }) + + it('should not apply filter recursively on failed dirs when noRecurseOnFailedFilter is true', done => { + const srcDir = path.join(TEST_DIR, 'src') + const srcFile1 = path.join(srcDir, '1.js') + const srcFile2 = path.join(srcDir, '2.css') + const srcFile3 = path.join(srcDir, 'node_modules', '3.css') + fse.outputFileSync(srcFile1, 'file 1 stuff') + fse.outputFileSync(srcFile2, 'file 2 stuff') + fse.outputFileSync(srcFile3, 'file 3 stuff') + const destDir = path.join(TEST_DIR, 'dest') + const destFile1 = path.join(destDir, '1.js') + const destFile2 = path.join(destDir, '2.css') + const destFile3 = path.join(destDir, 'node_modules', '3.css') + + const filter = s => path.extname(s) === '.css' || s === srcDir + + fse.copy(srcDir, destDir, {filter: filter, noRecurseOnFailedFilter: true}, err => { + assert(!err) + assert(!fs.existsSync(destFile1)) + assert(fs.existsSync(destFile2)) + assert(!fs.existsSync(destFile3)) + done() + }) + }) }) }) }) diff --git a/lib/copy/__tests__/ncp/broken-symlink.test.js b/lib/copy/__tests__/ncp/broken-symlink.test.js index 781ac6d9..aa7b990e 100644 --- a/lib/copy/__tests__/ncp/broken-symlink.test.js +++ b/lib/copy/__tests__/ncp/broken-symlink.test.js @@ -3,13 +3,13 @@ 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') /* global afterEach, beforeEach, describe, it */ -describe('ncp broken symlink', function () { +describe('ncp broken symlink', () => { const TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'ncp-broken-symlinks') const src = path.join(TEST_DIR, 'src') const out = path.join(TEST_DIR, 'out') 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..284eeaa8 100644 --- a/lib/copy/copy.js +++ b/lib/copy/copy.js @@ -2,21 +2,33 @@ const fs = require('graceful-fs') const path = require('path') -const ncp = require('./ncp') -const mkdir = require('../mkdirs') -const pathExists = require('../path-exists').pathExists +const mkdirp = require('../mkdirs').mkdirs +const remove = require('../remove').remove +const utimes = require('../util/utimes').utimesMillis -function copy (src, dest, options, callback) { - if (typeof options === 'function' && !callback) { - callback = options +const DEST_NOENT = Symbol('DEST_NOENT') +const DEST_EXISTS = Symbol('DEST_EXISTS') + +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`) @@ -24,31 +36,241 @@ 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.')) + + let stat = options.dereference ? fs.stat : fs.lstat + stat(src, (err, st) => { + if (err) return cb(err) + + if (st.isDirectory()) { + if (options.filter) return filterDir() + return onDir(st) + } else if (st.isFile() || st.isCharacterDevice() || st.isBlockDevice()) { + if (options.filter) return filterFile() + return onFile(st) + } else if (st.isSymbolicLink()) { + return onLink() } - pathExists(dir, (err, dirExists) => { + function filterDir () { + if (options.filter instanceof RegExp) { + console.warn('Warning: fs-extra: Passing a RegExp filter is deprecated, use a function') + if (!options.filter.test(src)) { + if (options.noRecurseOnFailedFilter) return cb() + return readFailedDir() + } + return onDir(st) + } else if (typeof options.filter === 'function') { + if (!options.filter(src, dest)) { + if (options.noRecurseOnFailedFilter) return cb() + return readFailedDir() + } + return onDir(st) + } + + function readFailedDir () { + 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(() => cb()).catch(cb) + }) + } + } + + function filterFile () { + if (options.filter instanceof RegExp) { + if (!options.filter.test(src)) return cb() + return onFile(st) + } else if (typeof options.filter === 'function') { + if (!options.filter(src, dest)) return cb() + return onFile(st) + } + } + }) + + // check dest to see if it exists and/or is a link + function checkDest (callback) { + mkdirp(path.dirname(dest), err => { 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) + fs.readlink(dest, (err, resolvedDestPath) => { + if (err) { + if (err.code === 'ENOENT') return callback(null, DEST_NOENT) + // dest exists but is not a link, Windows throws UNKNOWN error + if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return callback(null, DEST_EXISTS) + return callback(err) + } + // dest exists and is a link + return callback(null, resolvedDestPath) }) }) - }) + } + + function onFile (srcStat) { + checkDest((err, res) => { + if (err) return cb(err) + if (res === DEST_NOENT) { + return cpFile() + } else if (res === DEST_EXISTS) { + maybeCopy() + } else { // dest is a link + if (src === res) return cb() + maybeCopy() + } + }) + + function maybeCopy () { + if (options.overwrite) { + fs.unlink(dest, err => { + if (err) return cb(err) + return cpFile() + }) + } else if (options.errorOnExist) { + return cb(new Error(`'${dest}' already exists`)) + } else return cb() + } + + function cpFile () { + const rs = fs.createReadStream(src) + const ws = fs.createWriteStream(dest, { mode: srcStat.mode }) + + 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) + } + return cb() + }) + }) + } + } + + function onDir (srcStat) { + checkDest((err, res) => { + if (err) return cb(err) + if (res === DEST_NOENT) { + // if dest is a subdir of src, prevent copying into itself + if (isSrcKid(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() + }) + }) + } else if (res === DEST_EXISTS) { + if (isSrcKid(src, dest)) return cb(new Error(`Cannot copy directory '${src}' into itself '${dest}'`)) + return cpDir() + } else { // dest exists and is a link + if (src === res) return cb() + if (isSrcKid(src, res)) return cb(new Error(`Cannot copy directory '${src}' into itself '${res}'`)) + return cpDir() + } + }) + + function cpDir () { + 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(() => cb()).catch(cb) + }) + } + } + + function onLink () { + fs.readlink(src, (err, resolvedSrcPath) => { + if (err) return cb(err) + + if (options.dereference) { + resolvedSrcPath = path.resolve(process.cwd(), resolvedSrcPath) + } + + checkDest((err, resolvedDestPath) => { + if (err) return cb(err) + + if (resolvedDestPath === DEST_NOENT) { + // if dest is a subdir of resolved src path + if (isSrcKid(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 src points to dest + if (resolvedSrcPath === dest) return cb() + + // if dest is a subdir of src + if (isSrcKid(resolvedSrcPath, dest)) { + return cb(new Error(`Cannot copy directory '${resolvedSrcPath}' into itself '${dest}'`)) + } + + // if src is a subdir of dest, prevent making broken link + if (isSrcKid(dest, resolvedSrcPath)) { + return cb(new Error(`'${dest}' already exists and contains '${src}'`)) + } + + // we cannot use fs.unlink here since dest is not a link + remove(dest, err => { + if (err) return cb(err) + return fs.symlink(resolvedSrcPath, dest, cb) + }) + } else { // dest exists and is a link + if (options.dereference) { + resolvedDestPath = path.resolve(process.cwd(), resolvedDestPath) + } + if (resolvedSrcPath === resolvedDestPath) return cb() + + // if resolved dest path is a subdir of resolved src path, prevent copying into itself + if (isSrcKid(resolvedSrcPath, resolvedDestPath)) { + return cb(new Error(`Cannot copy directory '${resolvedSrcPath}' into itself '${resolvedDestPath}'`)) + } + + // if src is a subdir of dest, prevent making broken link + if (isSrcKid(resolvedDestPath, resolvedSrcPath)) { + return cb(new Error(`'${dest}' already exists and contains '${src}'`)) + } + + fs.unlink(dest, err => { + if (err) return cb(err) + return fs.symlink(resolvedSrcPath, dest, 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 isSrcKid (src, dest) { + src = path.resolve(src) + dest = path.resolve(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/index.js b/lib/move/index.js index c44e66be..b491c7d8 100644 --- a/lib/move/index.js +++ b/lib/move/index.js @@ -8,8 +8,8 @@ const u = require('universalify').fromCallback 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 @@ -143,14 +143,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) })