[Pkg-javascript-commits] [node-fs-extra] 01/05: New upstream version 5.0.0

Julien Puydt julien.puydt at laposte.net
Wed Feb 21 17:51:43 UTC 2018


This is an automated email from the git hooks/post-receive script.

jpuydt-guest pushed a commit to branch master
in repository node-fs-extra.

commit f429e4c6bade1a4ad1abb7d2383fadb4273b127d
Author: Julien Puydt <julien.puydt at laposte.net>
Date:   Wed Feb 21 18:36:47 2018 +0100

    New upstream version 5.0.0
---
 CHANGELOG.md                                       |  16 +
 docs/copy-sync.md                                  |   2 +-
 docs/copy.md                                       |   2 +-
 lib/copy-sync/__tests__/copy-sync-dir.test.js      |  27 ++
 .../__tests__/copy-sync-preserve-time.test.js      |  88 ++---
 .../copy-sync-prevent-copying-identical.test.js    | 181 ++++++++++
 .../copy-sync-prevent-copying-into-itself.test.js  | 372 +++++++++++++++++++++
 lib/copy-sync/__tests__/fixtures/a-file            |   1 -
 .../__tests__/fixtures/a-folder/another-file       |   1 -
 .../fixtures/a-folder/another-folder/file3         |   1 -
 lib/copy-sync/copy-file-sync.js                    |  41 ---
 lib/copy-sync/copy-sync.js                         | 239 ++++++++++---
 .../copy-prevent-copying-identical.test.js         | 192 +++++++++++
 .../copy-prevent-copying-into-itself.test.js       | 372 +++++++++++++++++++++
 lib/copy/__tests__/copy.test.js                    |  70 +++-
 lib/copy/__tests__/ncp/broken-symlink.test.js      |   2 +-
 lib/copy/__tests__/ncp/ncp-error-perm.test.js      |   2 +-
 lib/copy/__tests__/ncp/ncp.test.js                 |   2 +-
 lib/copy/__tests__/ncp/symlink.test.js             |   2 +-
 lib/copy/copy.js                                   | 276 +++++++++++++--
 lib/copy/ncp.js                                    | 234 -------------
 lib/move/index.js                                  |  10 +-
 lib/util/utimes.js                                 |   9 +-
 package.json                                       |   2 +-
 24 files changed, 1733 insertions(+), 411 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29a37b5..f5a8cf4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,19 @@
+5.0.0 / 2017-12-11
+------------------
+
+Significant refactor of `copy()` & `copySync()`, including breaking changes. No changes to other functions in this release.
+
+Huge thanks to **[@manidlou](https://github.com/manidlou)** for doing most of the work on this release.
+
+- The `filter` option can no longer be a RegExp (must be a function). This was deprecated since fs-extra v1.0.0. [#512](https://github.com/jprichardson/node-fs-extra/pull/512)
+- `copy()`'s `filter` option can now be a function that returns a Promise. [#518](https://github.com/jprichardson/node-fs-extra/pull/518)
+- `copy()` & `copySync()` now use `fs.copyFile()`/`fs.copyFileSync()` in environments that support it (currently Node 8.5.0+). Older Node versions still get the old implementation. [#505](https://github.com/jprichardson/node-fs-extra/pull/505)
+- Don't allow copying a directory into itself. [#83](https://github.com/jprichardson/node-fs-extra/issues/83)
+- Handle copying between identical files. [#198](https://github.com/jprichardson/node-fs-extra/issues/198)
+- Error out when copying an empty folder to a path that already exists. [#464](https://github.com/jprichardson/node-fs-extra/issues/464)
+- Don't create `dest`'s parent if the `filter` function aborts the `copy()` operation. [#517](https://github.com/jprichardson/node-fs-extra/pull/517)
+- Fix `writeStream` not being closed if there was an error in `copy()`. [#516](https://github.com/jprichardson/node-fs-extra/pull/516)
+
 4.0.3 / 2017-12-05
 ------------------
 
diff --git a/docs/copy-sync.md b/docs/copy-sync.md
index 8e61c2b..0a33e47 100644
--- a/docs/copy-sync.md
+++ b/docs/copy-sync.md
@@ -9,7 +9,7 @@ Copy a file or directory. The directory can have contents. Like `cp -r`.
   - `errorOnExist` `<boolean>`: when `overwrite` is `false` and the destination exists, throw an error. Default is `false`.
   - `dereference` `<boolean>`: dereference symlinks, default is `false`.
   - `preserveTimestamps` `<boolean>`: will set last modification and access times to the ones of the original source files, default is `false`.
-  - `filter` `<Function>`: Function to filter copied files. Return `true` to include, `false` to exclude. This can also be a RegExp, however this is deprecated (See [issue #239](https://github.com/jprichardson/node-fs-extra/issues/239) for background).
+  - `filter` `<Function>`: Function to filter copied files. Return `true` to include, `false` to exclude.
 
 ## Example:
 
diff --git a/docs/copy.md b/docs/copy.md
index 8440726..f7909d0 100644
--- a/docs/copy.md
+++ b/docs/copy.md
@@ -9,7 +9,7 @@ Copy a file or directory. The directory can have contents. Like `cp -r`.
   - `errorOnExist` `<boolean>`: when `overwrite` is `false` and the destination exists, throw an error. Default is `false`.
   - `dereference` `<boolean>`: dereference symlinks, default is `false`.
   - `preserveTimestamps` `<boolean>`: will set last modification and access times to the ones of the original source files, default is `false`.
-  - `filter` `<Function>`: Function to filter copied files. Return `true` to include, `false` to exclude. This can also be a RegExp, however this is deprecated (See [issue #239](https://github.com/jprichardson/node-fs-extra/issues/239) for background).
+  - `filter` `<Function>`: Function to filter copied files. Return `true` to include, `false` to exclude. Can also return a `Promise` that resolves to `true` or `false` (or pass in an `async` function).
 - `callback` `<Function>`
 
 ## Example:
diff --git a/lib/copy-sync/__tests__/copy-sync-dir.test.js b/lib/copy-sync/__tests__/copy-sync-dir.test.js
index 82c360e..e31360b 100644
--- a/lib/copy-sync/__tests__/copy-sync-dir.test.js
+++ b/lib/copy-sync/__tests__/copy-sync-dir.test.js
@@ -21,6 +21,21 @@ describe('+ copySync()', () => {
   })
 
   describe('> when the source is a directory', () => {
+    describe('> when dest exists and is a file', () => {
+      it('should throw error', () => {
+        const src = path.join(TEST_DIR, 'src')
+        const dest = path.join(TEST_DIR, 'file.txt')
+        fs.mkdirSync(src)
+        fs.ensureFileSync(dest)
+
+        try {
+          fs.copySync(src, dest)
+        } catch (err) {
+          assert.strictEqual(err.message, `Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
+        }
+      })
+    })
+
     it('should copy the directory synchronously', () => {
       const FILES = 2
 
@@ -89,6 +104,18 @@ describe('+ copySync()', () => {
   })
 
   describe('> when filter is used', () => {
+    it('should do nothing if filter fails', () => {
+      const srcDir = path.join(TEST_DIR, 'src')
+      const srcFile = path.join(srcDir, 'srcfile.css')
+      fs.outputFileSync(srcFile, 'src contents')
+      const destDir = path.join(TEST_DIR, 'dest')
+      const destFile = path.join(destDir, 'destfile.css')
+      const filter = s => path.extname(s) !== '.css' && !fs.statSync(s).isDirectory()
+
+      fs.copySync(srcFile, destFile, filter)
+      assert(!fs.existsSync(destDir))
+    })
+
     it('should should apply filter recursively', () => {
       const FILES = 2
       // Don't match anything that ends with a digit higher than 0:
diff --git a/lib/copy-sync/__tests__/copy-sync-preserve-time.test.js b/lib/copy-sync/__tests__/copy-sync-preserve-time.test.js
index 6a4ad9f..6775a4d 100644
--- a/lib/copy-sync/__tests__/copy-sync-preserve-time.test.js
+++ b/lib/copy-sync/__tests__/copy-sync-preserve-time.test.js
@@ -1,66 +1,76 @@
 'use strict'
 
-const fs = require('fs')
+const fs = require(process.cwd())
 const os = require('os')
 const path = require('path')
 const utimes = require('../../util/utimes')
 const assert = require('assert')
-const copySync = require('../copy-sync')
+const nodeVersion = process.versions.node
+const nodeVersionMajor = parseInt(nodeVersion.split('.')[0], 10)
 
-/* global beforeEach, describe, it */
+/* global beforeEach, afterEach, describe, it */
 
 if (process.arch === 'ia32') console.warn('32 bit arch; skipping copySync timestamp tests')
+if (nodeVersionMajor < 8) console.warn(`old node version (v${nodeVersion}); skipping copySync timestamp tests`)
 
-const describeIf64 = process.arch === 'ia32' ? describe.skip : describe
+const describeIfPractical = (process.arch === 'ia32' || nodeVersionMajor < 8) ? describe.skip : describe
 
-describeIf64('copySync', () => {
-  let TEST_DIR
+describeIfPractical('copySync() - preserveTimestamps option', () => {
+  let TEST_DIR, src, dest
 
   beforeEach(done => {
     TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-preserve-time')
-    require(process.cwd()).emptyDir(TEST_DIR, done)
+    fs.emptyDir(TEST_DIR, done)
   })
 
-  describe('> modification option', () => {
-    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')]
+  afterEach(done => fs.remove(TEST_DIR, done))
 
-    describe('> when modified option is turned off', () => {
-      it('should have different timestamps on copy', () => {
-        const from = path.join(SRC_FIXTURES_DIR)
-        copySync(from, TEST_DIR, {preserveTimestamps: false})
+  const FILES = ['a-file', path.join('a-folder', 'another-file'), path.join('a-folder', 'another-folder', 'file3')]
+
+  describe('> when preserveTimestamps option is false', () => {
+    it('should have different timestamps on copy', done => {
+      src = path.join(TEST_DIR, 'src')
+      dest = path.join(TEST_DIR, 'dest')
+      FILES.forEach(f => fs.ensureFileSync(path.join(src, f)))
+
+      setTimeout(() => {
+        fs.copySync(src, dest, {preserveTimestamps: false})
         FILES.forEach(testFile({preserveTimestamps: false}))
-      })
+        done()
+      }, 100)
     })
+  })
 
-    describe('> when modified option is turned on', () => {
-      it('should have the same timestamps on copy', () => {
-        const from = path.join(SRC_FIXTURES_DIR)
-        copySync(from, TEST_DIR, {preserveTimestamps: true})
-        FILES.forEach(testFile({preserveTimestamps: true}))
-      })
+  describe('> when preserveTimestamps option is true', () => {
+    it('should have the same timestamps on copy', () => {
+      src = path.join(TEST_DIR, 'src')
+      dest = path.join(TEST_DIR, 'dest')
+      FILES.forEach(f => fs.ensureFileSync(path.join(src, f)))
+
+      fs.copySync(src, dest, {preserveTimestamps: true})
+      FILES.forEach(testFile({preserveTimestamps: true}))
     })
+  })
 
-    function testFile (options) {
-      return function (file) {
-        const a = path.join(SRC_FIXTURES_DIR, file)
-        const b = path.join(TEST_DIR, file)
-        const fromStat = fs.statSync(a)
-        const toStat = fs.statSync(b)
-        if (options.preserveTimestamps) {
-          // https://github.com/nodejs/io.js/issues/2069
-          if (process.platform !== 'win32') {
-            assert.strictEqual(toStat.mtime.getTime(), fromStat.mtime.getTime())
-            assert.strictEqual(toStat.atime.getTime(), fromStat.atime.getTime())
-          } else {
-            assert.strictEqual(toStat.mtime.getTime(), utimes.timeRemoveMillis(fromStat.mtime.getTime()))
-            assert.strictEqual(toStat.atime.getTime(), utimes.timeRemoveMillis(fromStat.atime.getTime()))
-          }
+  function testFile (options) {
+    return function (file) {
+      const a = path.join(src, file)
+      const b = path.join(dest, file)
+      const fromStat = fs.statSync(a)
+      const toStat = fs.statSync(b)
+      if (options.preserveTimestamps) {
+        // https://github.com/nodejs/io.js/issues/2069
+        if (process.platform !== 'win32') {
+          assert.strictEqual(toStat.mtime.getTime(), fromStat.mtime.getTime())
+          assert.strictEqual(toStat.atime.getTime(), fromStat.atime.getTime())
         } else {
-          assert.notEqual(toStat.mtime.getTime(), fromStat.mtime.getTime())
-          // the access time might actually be the same, so check only modification time
+          assert.strictEqual(toStat.mtime.getTime(), utimes.timeRemoveMillis(fromStat.mtime.getTime()))
+          assert.strictEqual(toStat.atime.getTime(), utimes.timeRemoveMillis(fromStat.atime.getTime()))
         }
+      } else {
+        assert.notEqual(toStat.mtime.getTime(), fromStat.mtime.getTime())
+        // the access time might actually be the same, so check only modification time
       }
     }
-  })
+  }
 })
diff --git a/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js b/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js
new file mode 100644
index 0000000..1e0f598
--- /dev/null
+++ b/lib/copy-sync/__tests__/copy-sync-prevent-copying-identical.test.js
@@ -0,0 +1,181 @@
+'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', () => {
+    const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_copy_sync')
+    const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_copy_sync')
+    try {
+      fs.copySync(fileSrc, fileDest)
+    } catch (err) {
+      assert.equal(err.message, 'Source and destination must not be the same.')
+    }
+  })
+
+  // 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', () => {
+        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.copySync(src, destLink)
+
+        const newlen = klawSync(src).length
+        assert.strictEqual(newlen, oldlen)
+        const link = fs.readlinkSync(destLink)
+        assert.strictEqual(link, src)
+      })
+    })
+
+    describe(`>> when src is a symlink that points to a regular dest`, () => {
+      it('should throw error', () => {
+        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
+
+        try {
+          fs.copySync(srcLink, dest)
+        } catch (err) {
+          assert(err)
+        }
+
+        // assert nothing copied
+        const newlen = klawSync(dest).length
+        assert.strictEqual(newlen, oldlen)
+        const link = fs.readlinkSync(srcLink)
+        assert.strictEqual(link, dest)
+      })
+    })
+
+    describe('>> when src and dest are symlinks that point to the exact same path', () => {
+      it('should not copy and return', () => {
+        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.copySync(srcLink, destLink)
+
+        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)
+      })
+    })
+  })
+
+  // 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', () => {
+        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.copySync(src, destLink)
+
+        const link = fs.readlinkSync(destLink)
+        assert.strictEqual(link, src)
+        assert(fs.readFileSync(link, 'utf8'), 'some data')
+      })
+    })
+
+    describe(`>> when src is a symlink that points to a regular dest`, () => {
+      it('should throw error', () => {
+        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')
+
+        try {
+          fs.copySync(srcLink, dest)
+        } catch (err) {
+          assert.ok(err)
+        }
+        const link = fs.readlinkSync(srcLink)
+        assert.strictEqual(link, dest)
+        assert(fs.readFileSync(link, 'utf8'), 'some data')
+      })
+    })
+
+    describe('>> when src and dest are symlinks that point to the exact same path', () => {
+      it('should not copy and return', () => {
+        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.copySync(srcLink, destLink)
+
+        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')
+      })
+    })
+  })
+})
diff --git a/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js b/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js
new file mode 100644
index 0000000..fdac189
--- /dev/null
+++ b/lib/copy-sync/__tests__/copy-sync-prevent-copying-into-itself.test.js
@@ -0,0 +1,372 @@
+'use strict'
+
+const assert = require('assert')
+const os = require('os')
+const path = require('path')
+const fs = require(process.cwd())
+const klawSync = require('klaw-sync')
+
+/* global beforeEach, afterEach, describe, it */
+
+// these files are used for all tests
+const FILES = [
+  'file0.txt',
+  path.join('dir1', 'file1.txt'),
+  path.join('dir1', 'dir2', 'file2.txt'),
+  path.join('dir1', 'dir2', 'dir3', 'file3.txt')
+]
+
+const dat0 = 'file0'
+const dat1 = 'file1'
+const dat2 = 'file2'
+const dat3 = 'file3'
+
+describe('+ copySync() - prevent copying into itself', () => {
+  let TEST_DIR, src
+
+  beforeEach(done => {
+    TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-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 parent is a subdir of src', done => {
+      const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt')
+      const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt')
+      fs.writeFileSync(srcFile, dat0)
+
+      fs.copy(srcFile, destFile, err => {
+        assert.ifError(err)
+
+        assert(fs.existsSync(destFile, 'file copied'))
+        const out = fs.readFileSync(destFile, 'utf8')
+        assert.strictEqual(out, dat0, 'file contents matched')
+        done()
+      })
+    })
+  })
+
+  // test for these cases:
+  //  - src is directory, dest is directory
+  //  - src is directory, dest is symlink
+  //  - src is symlink, dest is directory
+  //  - src is symlink, dest is symlink
+
+  describe('> when source is a directory', () => {
+    describe('>> when dest is a directory', () => {
+      it(`should copy the directory successfully when dest is 'src_dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src_dest')
+        return testSuccess(src, dest, done)
+      })
+      it(`should copy the directory successfully when dest is 'src-dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src-dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest_src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest_src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src_dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src_dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src-dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src-dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest_src/src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest_src', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src_src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src_src', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src-src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src-src', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'srcsrc', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it('should copy the directory successfully when dest is very nested that all its parents need to be created', done => {
+        const dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo',
+          'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply',
+          'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/src_dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'src_dest')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest_src'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest_src')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest', 'src')
+        return testError(src, dest, done)
+      })
+    })
+
+    describe('>> when dest is a symlink', () => {
+      it('should not copy and return when dest points exactly to src', done => {
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const srclenBefore = klawSync(src).length
+        assert(srclenBefore > 2)
+
+        fs.copy(src, destLink, err => {
+          assert.ifError(err)
+
+          const srclenAfter = klawSync(src).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+
+          const link = fs.readlinkSync(destLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should copy the directory successfully when src is a subdir of resolved dest path', done => {
+        const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.copySync(src, srcInDest) // put some stuff in srcInDest
+
+        const dest = path.join(TEST_DIR, 'dest')
+        fs.symlinkSync(dest, destLink, 'dir')
+
+        const srclen = klawSync(srcInDest).length
+        const destlenBefore = klawSync(dest).length
+
+        assert(srclen > 2)
+        fs.copy(srcInDest, destLink, err => {
+          assert.ifError(err)
+
+          const destlenAfter = klawSync(dest).length
+
+          // assert dest length is oldlen + length of stuff copied from src
+          assert.strictEqual(destlenAfter, destlenBefore + srclen, 'dest length should be equal to old length + copied legnth')
+
+          FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied'))
+
+          const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8')
+          const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8')
+          const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8')
+          const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8')
+
+          assert.strictEqual(o0, dat0, 'files contents matched')
+          assert.strictEqual(o1, dat1, 'files contents matched')
+          assert.strictEqual(o2, dat2, 'files contents matched')
+          assert.strictEqual(o3, dat3, 'files contents matched')
+          done()
+        })
+      })
+    })
+  })
+
+  describe('> when source is a symlink', () => {
+    describe('>> when dest is a directory', () => {
+      it('should error when resolved src path points to dest', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src')
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          // assert source not affected
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should error when dest is a subdir of resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
+        fs.mkdirsSync(dest)
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should error when resolved src path is a subdir of dest', done => {
+        const dest = path.join(TEST_DIR, 'dest')
+
+        const resolvedSrcPath = path.join(dest, 'contains', 'src')
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.copySync(src, resolvedSrcPath)
+
+        // make symlink that points to a subdir in dest
+        fs.symlinkSync(resolvedSrcPath, srcLink, 'dir')
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          done()
+        })
+      })
+
+      it(`should copy the directory successfully when dest is 'src_src/dest'`, done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src_src', 'dest')
+        testSuccess(srcLink, dest, () => {
+          const link = fs.readlinkSync(dest)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'srcsrc', 'dest')
+        testSuccess(srcLink, dest, () => {
+          const link = fs.readlinkSync(dest)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+    })
+
+    describe('>> when dest is a symlink', () => {
+      it('should silently return when resolved dest path is exactly the same as resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const srclenBefore = klawSync(srcLink).length
+        const destlenBefore = klawSync(destLink).length
+        assert(srclenBefore > 2)
+        assert(destlenBefore > 2)
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+
+          const srclenAfter = klawSync(srcLink).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+          const destlenAfter = klawSync(destLink).length
+          assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change')
+
+          const srcln = fs.readlinkSync(srcLink)
+          assert.strictEqual(srcln, src)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          done()
+        })
+      })
+
+      it('should error when resolved dest path is a subdir of resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        const resolvedDestPath = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
+        fs.ensureFileSync(path.join(resolvedDestPath, 'subdir', 'somefile.txt'))
+
+        fs.symlinkSync(resolvedDestPath, destLink, 'dir')
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          done()
+        })
+      })
+
+      it('should error when resolved src path is a subdir of resolved dest path', done => {
+        const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+
+        const dest = path.join(TEST_DIR, 'dest')
+
+        fs.mkdirSync(dest)
+
+        fs.symlinkSync(srcInDest, srcLink, 'dir')
+        fs.symlinkSync(dest, destLink, 'dir')
+
+        fs.copy(srcLink, destLink, err => {
+          assert.strictEqual(err.message, `Cannot overwrite '${dest}' with '${srcInDest}'.`)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, dest)
+          done()
+        })
+      })
+    })
+  })
+})
+
+function testSuccess (src, dest, done) {
+  const srclen = klawSync(src).length
+  assert(srclen > 2)
+  fs.copy(src, dest, err => {
+    assert.ifError(err)
+
+    const destlen = klawSync(dest).length
+
+    assert.strictEqual(destlen, srclen)
+
+    FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied'))
+
+    const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8')
+    const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8')
+    const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8')
+    const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8')
+
+    assert.strictEqual(o0, dat0, 'file contents matched')
+    assert.strictEqual(o1, dat1, 'file contents matched')
+    assert.strictEqual(o2, dat2, 'file contents matched')
+    assert.strictEqual(o3, dat3, 'file contents matched')
+    done()
+  })
+}
+
+function testError (src, dest, done) {
+  fs.copy(src, dest, err => {
+    assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
+    done()
+  })
+}
diff --git a/lib/copy-sync/__tests__/fixtures/a-file b/lib/copy-sync/__tests__/fixtures/a-file
deleted file mode 100644
index 94a709d..0000000
--- a/lib/copy-sync/__tests__/fixtures/a-file
+++ /dev/null
@@ -1 +0,0 @@
-sonic the hedgehog
diff --git a/lib/copy-sync/__tests__/fixtures/a-folder/another-file b/lib/copy-sync/__tests__/fixtures/a-folder/another-file
deleted file mode 100644
index 31340c7..0000000
--- a/lib/copy-sync/__tests__/fixtures/a-folder/another-file
+++ /dev/null
@@ -1 +0,0 @@
-tails
diff --git a/lib/copy-sync/__tests__/fixtures/a-folder/another-folder/file3 b/lib/copy-sync/__tests__/fixtures/a-folder/another-folder/file3
deleted file mode 100644
index 73a394d..0000000
--- a/lib/copy-sync/__tests__/fixtures/a-folder/another-folder/file3
+++ /dev/null
@@ -1 +0,0 @@
-knuckles
diff --git a/lib/copy-sync/copy-file-sync.js b/lib/copy-sync/copy-file-sync.js
deleted file mode 100644
index 102a6be..0000000
--- a/lib/copy-sync/copy-file-sync.js
+++ /dev/null
@@ -1,41 +0,0 @@
-'use strict'
-
-const fs = require('graceful-fs')
-
-const BUF_LENGTH = 64 * 1024
-const _buff = require('../util/buffer')(BUF_LENGTH)
-
-function copyFileSync (srcFile, destFile, options) {
-  const overwrite = options.overwrite
-  const errorOnExist = options.errorOnExist
-  const preserveTimestamps = options.preserveTimestamps
-
-  if (fs.existsSync(destFile)) {
-    if (overwrite) {
-      fs.unlinkSync(destFile)
-    } else if (errorOnExist) {
-      throw new Error(`${destFile} already exists`)
-    } else return
-  }
-
-  const fdr = fs.openSync(srcFile, 'r')
-  const stat = fs.fstatSync(fdr)
-  const fdw = fs.openSync(destFile, 'w', stat.mode)
-  let bytesRead = 1
-  let pos = 0
-
-  while (bytesRead > 0) {
-    bytesRead = fs.readSync(fdr, _buff, 0, BUF_LENGTH, pos)
-    fs.writeSync(fdw, _buff, 0, bytesRead)
-    pos += bytesRead
-  }
-
-  if (preserveTimestamps) {
-    fs.futimesSync(fdw, stat.atime, stat.mtime)
-  }
-
-  fs.closeSync(fdr)
-  fs.closeSync(fdw)
-}
-
-module.exports = copyFileSync
diff --git a/lib/copy-sync/copy-sync.js b/lib/copy-sync/copy-sync.js
index 9d5639c..c4742db 100644
--- a/lib/copy-sync/copy-sync.js
+++ b/lib/copy-sync/copy-sync.js
@@ -2,61 +2,208 @@
 
 const fs = require('graceful-fs')
 const path = require('path')
-const copyFileSync = require('./copy-file-sync')
-const mkdir = require('../mkdirs')
+const mkdirpSync = require('../mkdirs').mkdirsSync
+const utimesSync = require('../util/utimes.js').utimesMillisSync
 
-function copySync (src, dest, options) {
-  if (typeof options === 'function' || options instanceof RegExp) {
-    options = {filter: options}
-  }
-
-  options = options || {}
-  options.recursive = !!options.recursive
+const notExist = Symbol('notExist')
+const existsReg = Symbol('existsReg')
 
-  // 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
+function copySync (src, dest, opts) {
+  if (typeof opts === 'function') {
+    opts = {filter: opts}
+  }
 
-  options.filter = options.filter || function () { return true }
+  opts = opts || {}
+  opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
+  opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
 
-  // Warn about using preserveTimestamps on 32-bit node:
-  if (options.preserveTimestamps && process.arch === 'ia32') {
+  // Warn about using preserveTimestamps on 32-bit node
+  if (opts.preserveTimestamps && process.arch === 'ia32') {
     console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n
     see https://github.com/jprichardson/node-fs-extra/issues/269`)
   }
 
-  const stats = (options.recursive && !options.dereference) ? fs.lstatSync(src) : fs.statSync(src)
-  const destFolder = path.dirname(dest)
-  const destFolderExists = fs.existsSync(destFolder)
-  let performCopy = false
-
-  if (options.filter instanceof RegExp) {
-    console.warn('Warning: fs-extra: Passing a RegExp filter is deprecated, use a function')
-    performCopy = options.filter.test(src)
-  } else if (typeof options.filter === 'function') performCopy = options.filter(src, dest)
-
-  if (stats.isFile() && performCopy) {
-    if (!destFolderExists) mkdir.mkdirsSync(destFolder)
-    copyFileSync(src, dest, {
-      overwrite: options.overwrite,
-      errorOnExist: options.errorOnExist,
-      preserveTimestamps: options.preserveTimestamps
-    })
-  } else if (stats.isDirectory() && performCopy) {
-    if (!fs.existsSync(dest)) mkdir.mkdirsSync(dest)
-    const contents = fs.readdirSync(src)
-    contents.forEach(content => {
-      const opts = options
-      opts.recursive = true
-      copySync(path.join(src, content), path.join(dest, content), opts)
-    })
-  } else if (options.recursive && stats.isSymbolicLink() && performCopy) {
-    const srcPath = fs.readlinkSync(src)
-    fs.symlinkSync(srcPath, dest)
+  src = path.resolve(src)
+  dest = path.resolve(dest)
+
+  // don't allow src and dest to be the same
+  if (src === dest) throw new Error('Source and destination must not be the same.')
+
+  if (opts.filter && !opts.filter(src, dest)) return
+
+  const destParent = path.dirname(dest)
+  if (!fs.existsSync(destParent)) mkdirpSync(destParent)
+  return startCopy(src, dest, opts)
+}
+
+function startCopy (src, dest, opts) {
+  if (opts.filter && !opts.filter(src, dest)) return
+  return getStats(src, dest, opts)
+}
+
+function getStats (src, dest, opts) {
+  const statSync = opts.dereference ? fs.statSync : fs.lstatSync
+  const st = statSync(src)
+
+  if (st.isDirectory()) return onDir(st, src, dest, opts)
+  else if (st.isFile() ||
+           st.isCharacterDevice() ||
+           st.isBlockDevice()) return onFile(st, src, dest, opts)
+  else if (st.isSymbolicLink()) return onLink(src, dest, opts)
+}
+
+function onFile (srcStat, src, dest, opts) {
+  const resolvedPath = checkDest(dest)
+  if (resolvedPath === notExist) {
+    return copyFile(srcStat, src, dest, opts)
+  } else if (resolvedPath === existsReg) {
+    return mayCopyFile(srcStat, src, dest, opts)
+  } else {
+    if (src === resolvedPath) return
+    return mayCopyFile(srcStat, src, dest, opts)
+  }
+}
+
+function mayCopyFile (srcStat, src, dest, opts) {
+  if (opts.overwrite) {
+    fs.unlinkSync(dest)
+    return copyFile(srcStat, src, dest, opts)
+  } else if (opts.errorOnExist) {
+    throw new Error(`'${dest}' already exists`)
+  }
+}
+
+function copyFile (srcStat, src, dest, opts) {
+  if (typeof fs.copyFileSync === 'function') {
+    fs.copyFileSync(src, dest)
+    fs.chmodSync(dest, srcStat.mode)
+    if (opts.preserveTimestamps) {
+      return utimesSync(dest, srcStat.atime, srcStat.mtime)
+    }
+    return
+  }
+  return copyFileFallback(srcStat, src, dest, opts)
+}
+
+function copyFileFallback (srcStat, src, dest, opts) {
+  const BUF_LENGTH = 64 * 1024
+  const _buff = require('../util/buffer')(BUF_LENGTH)
+
+  const fdr = fs.openSync(src, 'r')
+  const fdw = fs.openSync(dest, 'w', srcStat.mode)
+  let bytesRead = 1
+  let pos = 0
+
+  while (bytesRead > 0) {
+    bytesRead = fs.readSync(fdr, _buff, 0, BUF_LENGTH, pos)
+    fs.writeSync(fdw, _buff, 0, bytesRead)
+    pos += bytesRead
+  }
+
+  if (opts.preserveTimestamps) fs.futimesSync(fdw, srcStat.atime, srcStat.mtime)
+
+  fs.closeSync(fdr)
+  fs.closeSync(fdw)
+}
+
+function onDir (srcStat, src, dest, opts) {
+  const resolvedPath = checkDest(dest)
+  if (resolvedPath === notExist) {
+    if (isSrcSubdir(src, dest)) {
+      throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
+    }
+    return mkDirAndCopy(srcStat, src, dest, opts)
+  } else if (resolvedPath === existsReg) {
+    if (isSrcSubdir(src, dest)) {
+      throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
+    }
+    return mayCopyDir(src, dest, opts)
+  } else {
+    if (src === resolvedPath) return
+    return copyDir(src, dest, opts)
+  }
+}
+
+function mayCopyDir (src, dest, opts) {
+  if (!fs.statSync(dest).isDirectory()) {
+    throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
+  }
+  return copyDir(src, dest, opts)
+}
+
+function mkDirAndCopy (srcStat, src, dest, opts) {
+  fs.mkdirSync(dest, srcStat.mode)
+  fs.chmodSync(dest, srcStat.mode)
+  return copyDir(src, dest, opts)
+}
+
+function copyDir (src, dest, opts) {
+  fs.readdirSync(src).forEach(item => {
+    startCopy(path.join(src, item), path.join(dest, item), opts)
+  })
+}
+
+function onLink (src, dest, opts) {
+  let resolvedSrcPath = fs.readlinkSync(src)
+
+  if (opts.dereference) {
+    resolvedSrcPath = path.resolve(process.cwd(), resolvedSrcPath)
+  }
+
+  let resolvedDestPath = checkDest(dest)
+  if (resolvedDestPath === notExist || resolvedDestPath === existsReg) {
+    // if dest already exists, fs throws error anyway,
+    // so no need to guard against it here.
+    return fs.symlinkSync(resolvedSrcPath, dest)
+  } else {
+    if (opts.dereference) {
+      resolvedDestPath = path.resolve(process.cwd(), resolvedDestPath)
+    }
+    if (resolvedDestPath === resolvedSrcPath) return
+
+    // prevent copy if src is a subdir of dest since unlinking
+    // dest in this case would result in removing src contents
+    // and therefore a broken symlink would be created.
+    if (fs.statSync(dest).isDirectory() && isSrcSubdir(resolvedDestPath, resolvedSrcPath)) {
+      throw new Error(`Cannot overwrite '${resolvedDestPath}' with '${resolvedSrcPath}'.`)
+    }
+    return copyLink(resolvedSrcPath, dest)
+  }
+}
+
+function copyLink (resolvedSrcPath, dest) {
+  fs.unlinkSync(dest)
+  return fs.symlinkSync(resolvedSrcPath, dest)
+}
+
+// check if dest exists and/or is a symlink
+function checkDest (dest) {
+  let resolvedPath
+  try {
+    resolvedPath = fs.readlinkSync(dest)
+  } catch (err) {
+    if (err.code === 'ENOENT') return notExist
+
+    // dest exists and is a regular file or directory, Windows may throw UNKNOWN error
+    if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return existsReg
+
+    throw err
+  }
+  return resolvedPath // dest exists and is a symlink
+}
+
+// return true if dest is a subdir of src, otherwise false.
+// extract dest base dir and check if that is the same as src basename
+function isSrcSubdir (src, dest) {
+  const baseDir = dest.split(path.dirname(src) + path.sep)[1]
+  if (baseDir) {
+    const destBasename = baseDir.split(path.sep)[0]
+    if (destBasename) {
+      return src !== dest && dest.indexOf(src) > -1 && destBasename === path.basename(src)
+    }
+    return false
   }
+  return false
 }
 
 module.exports = copySync
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 0000000..615645b
--- /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('+ copy() - prevent copying identical files and dirs', () => {
+  let TEST_DIR = ''
+  let src = ''
+  let dest = ''
+
+  beforeEach(done => {
+    TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-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')
+    const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_copy')
+
+    fs.copy(fileSrc, fileDest, err => {
+      assert.equal(err.message, 'Source and destination must not be the same.')
+      done()
+    })
+  })
+
+  // src is directory:
+  //  src is regular, dest is symlink
+  //  src is symlink, dest is regular
+  //  src is symlink, dest is symlink
+
+  describe('> when the source is a directory', () => {
+    describe(`>> when src is regular and dest is a symlink that points to src`, () => {
+      it('should not copy and return', done => {
+        src = path.join(TEST_DIR, 'src')
+        fs.mkdirsSync(src)
+        const subdir = path.join(TEST_DIR, 'src', 'subdir')
+        fs.mkdirsSync(subdir)
+        fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data')
+
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const oldlen = klawSync(src).length
+
+        fs.copy(src, destLink, err => {
+          assert.ifError(err)
+
+          const newlen = klawSync(src).length
+          assert.strictEqual(newlen, oldlen)
+          const link = fs.readlinkSync(destLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+    })
+
+    describe(`>> when src is a symlink that points to a regular dest`, () => {
+      it('should throw error', done => {
+        dest = path.join(TEST_DIR, 'dest')
+        fs.mkdirsSync(dest)
+        const subdir = path.join(TEST_DIR, 'dest', 'subdir')
+        fs.mkdirsSync(subdir)
+        fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data')
+
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(dest, srcLink, 'dir')
+
+        const oldlen = klawSync(dest).length
+
+        fs.copy(srcLink, dest, err => {
+          assert.ok(err)
+
+          // assert nothing copied
+          const newlen = klawSync(dest).length
+          assert.strictEqual(newlen, oldlen)
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, dest)
+          done()
+        })
+      })
+    })
+
+    describe('>> when src and dest are symlinks that point to the exact same path', () => {
+      it('should not copy and return', done => {
+        src = path.join(TEST_DIR, 'src')
+        fs.mkdirsSync(src)
+        const srcLink = path.join(TEST_DIR, 'src_symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+        const destLink = path.join(TEST_DIR, 'dest_symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const srclenBefore = klawSync(srcLink).length
+        const destlenBefore = klawSync(destLink).length
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+
+          const srclenAfter = klawSync(srcLink).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+          const destlenAfter = klawSync(destLink).length
+          assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change')
+
+          const srcln = fs.readlinkSync(srcLink)
+          assert.strictEqual(srcln, src)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          done()
+        })
+      })
+    })
+  })
+
+  // src is file:
+  //  src is regular, dest is symlink
+  //  src is symlink, dest is regular
+  //  src is symlink, dest is symlink
+
+  describe('> when the source is a file', () => {
+    describe(`>> when src is regular and dest is a symlink that points to src`, () => {
+      it('should not copy and return', done => {
+        src = path.join(TEST_DIR, 'src', 'somefile.txt')
+        fs.ensureFileSync(src)
+        fs.writeFileSync(src, 'some data')
+
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'file')
+
+        fs.copy(src, destLink, err => {
+          assert.ifError(err)
+
+          const link = fs.readlinkSync(destLink)
+          assert.strictEqual(link, src)
+          assert(fs.readFileSync(link, 'utf8'), 'some data')
+          done()
+        })
+      })
+    })
+
+    describe(`>> when src is a symlink that points to a regular dest`, () => {
+      it('should throw error', done => {
+        dest = path.join(TEST_DIR, 'dest', 'somefile.txt')
+        fs.ensureFileSync(dest)
+        fs.writeFileSync(dest, 'some data')
+
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(dest, srcLink, 'file')
+
+        fs.copy(srcLink, dest, err => {
+          assert.ok(err)
+
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, dest)
+          assert(fs.readFileSync(link, 'utf8'), 'some data')
+          done()
+        })
+      })
+    })
+
+    describe('>> when src and dest are symlinks that point to the exact same path', () => {
+      it('should not copy and return', done => {
+        src = path.join(TEST_DIR, 'src', 'srcfile.txt')
+        fs.ensureFileSync(src)
+        fs.writeFileSync(src, 'src data')
+
+        const srcLink = path.join(TEST_DIR, 'src_symlink')
+        fs.symlinkSync(src, srcLink, 'file')
+
+        const destLink = path.join(TEST_DIR, 'dest_symlink')
+        fs.symlinkSync(src, destLink, 'file')
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+
+          const srcln = fs.readlinkSync(srcLink)
+          assert.strictEqual(srcln, src)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          assert(fs.readFileSync(srcln, 'utf8'), 'src data')
+          assert(fs.readFileSync(destln, 'utf8'), 'src data')
+          done()
+        })
+      })
+    })
+  })
+})
diff --git a/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js
new file mode 100644
index 0000000..9fe0ef0
--- /dev/null
+++ b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js
@@ -0,0 +1,372 @@
+'use strict'
+
+const assert = require('assert')
+const os = require('os')
+const path = require('path')
+const fs = require(process.cwd())
+const klawSync = require('klaw-sync')
+
+/* global beforeEach, afterEach, describe, it */
+
+// these files are used for all tests
+const FILES = [
+  'file0.txt',
+  path.join('dir1', 'file1.txt'),
+  path.join('dir1', 'dir2', 'file2.txt'),
+  path.join('dir1', 'dir2', 'dir3', 'file3.txt')
+]
+
+const dat0 = 'file0'
+const dat1 = 'file1'
+const dat2 = 'file2'
+const dat3 = 'file3'
+
+describe('+ copy() - prevent copying into itself', () => {
+  let TEST_DIR, src
+
+  beforeEach(done => {
+    TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-prevent-copying-into-itself')
+    src = path.join(TEST_DIR, 'src')
+    fs.mkdirpSync(src)
+
+    fs.outputFileSync(path.join(src, FILES[0]), dat0)
+    fs.outputFileSync(path.join(src, FILES[1]), dat1)
+    fs.outputFileSync(path.join(src, FILES[2]), dat2)
+    fs.outputFileSync(path.join(src, FILES[3]), dat3)
+    done()
+  })
+
+  afterEach(done => fs.remove(TEST_DIR, done))
+
+  describe('> when source is a file', () => {
+    it('should copy the file successfully even when dest parent is a subdir of src', done => {
+      const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt')
+      const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt')
+      fs.writeFileSync(srcFile, dat0)
+
+      fs.copy(srcFile, destFile, err => {
+        assert.ifError(err)
+
+        assert(fs.existsSync(destFile, 'file copied'))
+        const out = fs.readFileSync(destFile, 'utf8')
+        assert.strictEqual(out, dat0, 'file contents matched')
+        done()
+      })
+    })
+  })
+
+  // test for these cases:
+  //  - src is directory, dest is directory
+  //  - src is directory, dest is symlink
+  //  - src is symlink, dest is directory
+  //  - src is symlink, dest is symlink
+
+  describe('> when source is a directory', () => {
+    describe('>> when dest is a directory', () => {
+      it(`should copy the directory successfully when dest is 'src_dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src_dest')
+        return testSuccess(src, dest, done)
+      })
+      it(`should copy the directory successfully when dest is 'src-dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src-dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest_src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest_src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src_dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src_dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src-dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src-dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest_src/src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest_src', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src_src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src_src', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src-src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src-src', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'srcsrc', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it('should copy the directory successfully when dest is very nested that all its parents need to be created', done => {
+        const dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo',
+          'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply',
+          'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/src_dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'src_dest')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest_src'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest_src')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest', 'src')
+        return testError(src, dest, done)
+      })
+    })
+
+    describe('>> when dest is a symlink', () => {
+      it('should not copy and return when dest points exactly to src', done => {
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const srclenBefore = klawSync(src).length
+        assert(srclenBefore > 2)
+
+        fs.copy(src, destLink, err => {
+          assert.ifError(err)
+
+          const srclenAfter = klawSync(src).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+
+          const link = fs.readlinkSync(destLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should copy the directory successfully when src is a subdir of resolved dest path', done => {
+        const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.copySync(src, srcInDest) // put some stuff in srcInDest
+
+        const dest = path.join(TEST_DIR, 'dest')
+        fs.symlinkSync(dest, destLink, 'dir')
+
+        const srclen = klawSync(srcInDest).length
+        const destlenBefore = klawSync(dest).length
+
+        assert(srclen > 2)
+        fs.copy(srcInDest, destLink, err => {
+          assert.ifError(err)
+
+          const destlenAfter = klawSync(dest).length
+
+          // assert dest length is oldlen + length of stuff copied from src
+          assert.strictEqual(destlenAfter, destlenBefore + srclen, 'dest length should be equal to old length + copied legnth')
+
+          FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied'))
+
+          const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8')
+          const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8')
+          const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8')
+          const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8')
+
+          assert.strictEqual(o0, dat0, 'files contents matched')
+          assert.strictEqual(o1, dat1, 'files contents matched')
+          assert.strictEqual(o2, dat2, 'files contents matched')
+          assert.strictEqual(o3, dat3, 'files contents matched')
+          done()
+        })
+      })
+    })
+  })
+
+  describe('> when source is a symlink', () => {
+    describe('>> when dest is a directory', () => {
+      it('should error when resolved src path points to dest', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src')
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          // assert source not affected
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should error when dest is a subdir of resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
+        fs.mkdirsSync(dest)
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should error when resolved src path is a subdir of dest', done => {
+        const dest = path.join(TEST_DIR, 'dest')
+
+        const resolvedSrcPath = path.join(dest, 'contains', 'src')
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.copySync(src, resolvedSrcPath)
+
+        // make symlink that points to a subdir in dest
+        fs.symlinkSync(resolvedSrcPath, srcLink, 'dir')
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          done()
+        })
+      })
+
+      it(`should copy the directory successfully when dest is 'src_src/dest'`, done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src_src', 'dest')
+        testSuccess(srcLink, dest, () => {
+          const link = fs.readlinkSync(dest)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'srcsrc', 'dest')
+        testSuccess(srcLink, dest, () => {
+          const link = fs.readlinkSync(dest)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+    })
+
+    describe('>> when dest is a symlink', () => {
+      it('should silently return when resolved dest path is exactly the same as resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const srclenBefore = klawSync(srcLink).length
+        const destlenBefore = klawSync(destLink).length
+        assert(srclenBefore > 2)
+        assert(destlenBefore > 2)
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+
+          const srclenAfter = klawSync(srcLink).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+          const destlenAfter = klawSync(destLink).length
+          assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change')
+
+          const srcln = fs.readlinkSync(srcLink)
+          assert.strictEqual(srcln, src)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          done()
+        })
+      })
+
+      it('should error when resolved dest path is a subdir of resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        const resolvedDestPath = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
+        fs.ensureFileSync(path.join(resolvedDestPath, 'subdir', 'somefile.txt'))
+
+        fs.symlinkSync(resolvedDestPath, destLink, 'dir')
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          done()
+        })
+      })
+
+      it('should error when resolved src path is a subdir of resolved dest path', done => {
+        const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+
+        const dest = path.join(TEST_DIR, 'dest')
+
+        fs.mkdirSync(dest)
+
+        fs.symlinkSync(srcInDest, srcLink, 'dir')
+        fs.symlinkSync(dest, destLink, 'dir')
+
+        fs.copy(srcLink, destLink, err => {
+          assert.strictEqual(err.message, `Cannot overwrite '${dest}' with '${srcInDest}'.`)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, dest)
+          done()
+        })
+      })
+    })
+  })
+})
+
+function testSuccess (src, dest, done) {
+  const srclen = klawSync(src).length
+  assert(srclen > 2)
+  fs.copy(src, dest, err => {
+    assert.ifError(err)
+
+    const destlen = klawSync(dest).length
+
+    assert.strictEqual(destlen, srclen)
+
+    FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied'))
+
+    const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8')
+    const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8')
+    const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8')
+    const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8')
+
+    assert.strictEqual(o0, dat0, 'file contents matched')
+    assert.strictEqual(o1, dat1, 'file contents matched')
+    assert.strictEqual(o2, dat2, 'file contents matched')
+    assert.strictEqual(o3, dat3, 'file contents matched')
+    done()
+  })
+}
+
+function testError (src, dest, done) {
+  fs.copy(src, dest, err => {
+    assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
+    done()
+  })
+}
diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js
index 6ed64ff..98692b4 100644
--- a/lib/copy/__tests__/copy.test.js
+++ b/lib/copy/__tests__/copy.test.js
@@ -30,6 +30,30 @@ describe('fs-extra', () => {
       })
     })
 
+    it('should error when overwrite=false and file exists', done => {
+      const src = path.join(TEST_DIR, 'src.txt')
+      const dest = path.join(TEST_DIR, 'dest.txt')
+
+      fse.ensureFileSync(src)
+      fse.ensureFileSync(dest)
+      fse.copy(src, dest, {overwrite: false, errorOnExist: true}, err => {
+        assert(err)
+        done()
+      })
+    })
+
+    it('should error when overwrite=false and file exists in a dir', done => {
+      const src = path.join(TEST_DIR, 'src', 'sfile.txt')
+      const dest = path.join(TEST_DIR, 'dest', 'dfile.txt')
+
+      fse.ensureFileSync(src)
+      fse.ensureFileSync(dest)
+      fse.copy(src, dest, {overwrite: false, errorOnExist: true}, err => {
+        assert(err)
+        done()
+      })
+    })
+
     describe('> when the source is a file', () => {
       it('should copy the file asynchronously', done => {
         const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_src')
@@ -98,6 +122,20 @@ describe('fs-extra', () => {
         })
       })
 
+      describe('> when dest exists and is a file', () => {
+        it('should return an error', done => {
+          const src = path.join(TEST_DIR, 'src')
+          const dest = path.join(TEST_DIR, 'file.txt')
+          fs.mkdirSync(src)
+          fse.ensureFileSync(dest)
+
+          fse.copy(src, dest, err => {
+            assert.strictEqual(err.message, `Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
+            done()
+          })
+        })
+      })
+
       it('should copy the directory asynchronously', done => {
         const FILES = 2
         const src = path.join(TEST_DIR, 'src')
@@ -170,6 +208,21 @@ describe('fs-extra', () => {
     })
 
     describe('> when filter is used', () => {
+      it('should do nothing if filter fails', done => {
+        const srcDir = path.join(TEST_DIR, 'src')
+        const srcFile = path.join(srcDir, 'srcfile.css')
+        fse.outputFileSync(srcFile, 'src contents')
+        const destDir = path.join(TEST_DIR, 'dest')
+        const destFile = path.join(destDir, 'destfile.css')
+        const filter = s => path.extname(s) !== '.css' && !fs.statSync(s).isDirectory()
+
+        fse.copy(srcFile, destFile, filter, err => {
+          assert.ifError(err)
+          assert(!fs.existsSync(destDir))
+          done()
+        })
+      })
+
       it('should only copy files allowed by filter fn', done => {
         const srcFile1 = path.join(TEST_DIR, '1.css')
         fs.writeFileSync(srcFile1, '')
@@ -196,7 +249,20 @@ describe('fs-extra', () => {
         })
       })
 
-      it('should should apply filter recursively', done => {
+      it('allows filter fn to return a promise', done => {
+        const srcFile1 = path.join(TEST_DIR, '1.css')
+        fs.writeFileSync(srcFile1, '')
+        const destFile1 = path.join(TEST_DIR, 'dest1.css')
+        const filter = s => Promise.resolve(s.split('.').pop() !== 'css')
+
+        fse.copy(srcFile1, destFile1, filter, err => {
+          assert(!err)
+          assert(!fs.existsSync(destFile1))
+          done()
+        })
+      })
+
+      it('should apply filter recursively', done => {
         const FILES = 2
         // Don't match anything that ends with a digit higher than 0:
         const filter = s => /(0|\D)$/i.test(s)
@@ -242,7 +308,7 @@ describe('fs-extra', () => {
         })
       })
 
-      it('should apply the filter to directory names', done => {
+      it('should apply filter to directory names', done => {
         const IGNORE = 'ignore'
         const filter = p => !~p.indexOf(IGNORE)
 
diff --git a/lib/copy/__tests__/ncp/broken-symlink.test.js b/lib/copy/__tests__/ncp/broken-symlink.test.js
index 781ac6d..278b2c1 100644
--- a/lib/copy/__tests__/ncp/broken-symlink.test.js
+++ b/lib/copy/__tests__/ncp/broken-symlink.test.js
@@ -3,7 +3,7 @@
 const fs = require('fs')
 const os = require('os')
 const fse = require(process.cwd())
-const ncp = require('../../ncp')
+const ncp = require('../../copy')
 const path = require('path')
 const assert = require('assert')
 
diff --git a/lib/copy/__tests__/ncp/ncp-error-perm.test.js b/lib/copy/__tests__/ncp/ncp-error-perm.test.js
index 451bbdf..18876e6 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 23ad3fc..31ae829 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 1b8816b..ff481a5 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 309a93d..fd9687f 100644
--- a/lib/copy/copy.js
+++ b/lib/copy/copy.js
@@ -2,53 +2,263 @@
 
 const fs = require('graceful-fs')
 const path = require('path')
-const ncp = require('./ncp')
-const mkdir = require('../mkdirs')
+const mkdirp = require('../mkdirs').mkdirs
 const pathExists = require('../path-exists').pathExists
+const utimes = require('../util/utimes').utimesMillis
 
-function copy (src, dest, options, callback) {
-  if (typeof options === 'function' && !callback) {
-    callback = options
-    options = {}
-  } else if (typeof options === 'function' || options instanceof RegExp) {
-    options = {filter: options}
+const notExist = Symbol('notExist')
+const existsReg = Symbol('existsReg')
+
+function copy (src, dest, opts, cb) {
+  if (typeof opts === 'function' && !cb) {
+    cb = opts
+    opts = {}
+  } else if (typeof opts === 'function') {
+    opts = {filter: opts}
   }
-  callback = callback || function () {}
-  options = options || {}
 
-  // Warn about using preserveTimestamps on 32-bit node:
-  if (options.preserveTimestamps && process.arch === 'ia32') {
+  cb = cb || function () {}
+  opts = opts || {}
+
+  opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
+  opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
+
+  // Warn about using preserveTimestamps on 32-bit node
+  if (opts.preserveTimestamps && process.arch === 'ia32') {
     console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n
     see https://github.com/jprichardson/node-fs-extra/issues/269`)
   }
 
+  src = path.resolve(src)
+  dest = path.resolve(dest)
+
   // don't allow src and dest to be the same
-  const basePath = process.cwd()
-  const currentPath = path.resolve(basePath, src)
-  const targetPath = path.resolve(basePath, dest)
-  if (currentPath === targetPath) return callback(new Error('Source and destination must not be the same.'))
-
-  fs.lstat(src, (err, stats) => {
-    if (err) return callback(err)
-
-    let dir = null
-    if (stats.isDirectory()) {
-      const parts = dest.split(path.sep)
-      parts.pop()
-      dir = parts.join(path.sep)
+  if (src === dest) return cb(new Error('Source and destination must not be the same.'))
+
+  if (opts.filter) return handleFilter(checkParentDir, src, dest, opts, cb)
+  return checkParentDir(src, dest, opts, cb)
+}
+
+function checkParentDir (src, dest, opts, cb) {
+  const destParent = path.dirname(dest)
+  pathExists(destParent, (err, dirExists) => {
+    if (err) return cb(err)
+    if (dirExists) return startCopy(src, dest, opts, cb)
+    mkdirp(destParent, err => {
+      if (err) return cb(err)
+      return startCopy(src, dest, opts, cb)
+    })
+  })
+}
+
+function startCopy (src, dest, opts, cb) {
+  if (opts.filter) return handleFilter(getStats, src, dest, opts, cb)
+  return getStats(src, dest, opts, cb)
+}
+
+function handleFilter (onInclude, src, dest, opts, cb) {
+  Promise.resolve(opts.filter(src, dest))
+    .then(include => {
+      if (include) return onInclude(src, dest, opts, cb)
+      return cb()
+    }, error => cb(error))
+}
+
+function getStats (src, dest, opts, cb) {
+  const stat = opts.dereference ? fs.stat : fs.lstat
+  stat(src, (err, st) => {
+    if (err) return cb(err)
+
+    if (st.isDirectory()) return onDir(st, src, dest, opts, cb)
+    else if (st.isFile() ||
+             st.isCharacterDevice() ||
+             st.isBlockDevice()) return onFile(st, src, dest, opts, cb)
+    else if (st.isSymbolicLink()) return onLink(src, dest, opts, cb)
+  })
+}
+
+function onFile (srcStat, src, dest, opts, cb) {
+  checkDest(dest, (err, resolvedPath) => {
+    if (err) return cb(err)
+    if (resolvedPath === notExist) {
+      return copyFile(srcStat, src, dest, opts, cb)
+    } else if (resolvedPath === existsReg) {
+      return mayCopyFile(srcStat, src, dest, opts, cb)
+    } else {
+      if (src === resolvedPath) return cb()
+      return mayCopyFile(srcStat, src, dest, opts, cb)
+    }
+  })
+}
+
+function mayCopyFile (srcStat, src, dest, opts, cb) {
+  if (opts.overwrite) {
+    fs.unlink(dest, err => {
+      if (err) return cb(err)
+      return copyFile(srcStat, src, dest, opts, cb)
+    })
+  } else if (opts.errorOnExist) {
+    return cb(new Error(`'${dest}' already exists`))
+  } else return cb()
+}
+
+function copyFile (srcStat, src, dest, opts, cb) {
+  if (typeof fs.copyFile === 'function') {
+    return fs.copyFile(src, dest, err => {
+      if (err) return cb(err)
+      return setDestModeAndTimestamps(srcStat, dest, opts, cb)
+    })
+  }
+  return copyFileFallback(srcStat, src, dest, opts, cb)
+}
+
+function copyFileFallback (srcStat, src, dest, opts, cb) {
+  const rs = fs.createReadStream(src)
+  rs.on('error', err => cb(err))
+    .once('open', () => {
+      const ws = fs.createWriteStream(dest, { mode: srcStat.mode })
+      ws.on('error', err => cb(err))
+        .on('open', () => rs.pipe(ws))
+        .once('close', () => setDestModeAndTimestamps(srcStat, dest, opts, cb))
+    })
+}
+
+function setDestModeAndTimestamps (srcStat, dest, opts, cb) {
+  fs.chmod(dest, srcStat.mode, err => {
+    if (err) return cb(err)
+    if (opts.preserveTimestamps) {
+      return utimes(dest, srcStat.atime, srcStat.mtime, cb)
+    }
+    return cb()
+  })
+}
+
+function onDir (srcStat, src, dest, opts, cb) {
+  checkDest(dest, (err, resolvedPath) => {
+    if (err) return cb(err)
+    if (resolvedPath === notExist) {
+      if (isSrcSubdir(src, dest)) {
+        return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
+      }
+      return mkDirAndCopy(srcStat, src, dest, opts, cb)
+    } else if (resolvedPath === existsReg) {
+      if (isSrcSubdir(src, dest)) {
+        return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
+      }
+      return mayCopyDir(src, dest, opts, cb)
     } else {
-      dir = path.dirname(dest)
+      if (src === resolvedPath) return cb()
+      return copyDir(src, dest, opts, cb)
+    }
+  })
+}
+
+function mayCopyDir (src, dest, opts, cb) {
+  fs.stat(dest, (err, st) => {
+    if (err) return cb(err)
+    if (!st.isDirectory()) {
+      return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`))
+    }
+    return copyDir(src, dest, opts, cb)
+  })
+}
+
+function mkDirAndCopy (srcStat, src, dest, opts, cb) {
+  fs.mkdir(dest, srcStat.mode, err => {
+    if (err) return cb(err)
+    fs.chmod(dest, srcStat.mode, err => {
+      if (err) return cb(err)
+      return copyDir(src, dest, opts, cb)
+    })
+  })
+}
+
+function copyDir (src, dest, opts, cb) {
+  fs.readdir(src, (err, items) => {
+    if (err) return cb(err)
+    return copyDirItems(items, src, dest, opts, cb)
+  })
+}
+
+function copyDirItems (items, src, dest, opts, cb) {
+  const item = items.pop()
+  if (!item) return cb()
+  startCopy(path.join(src, item), path.join(dest, item), opts, err => {
+    if (err) return cb(err)
+    return copyDirItems(items, src, dest, opts, cb)
+  })
+}
+
+function onLink (src, dest, opts, cb) {
+  fs.readlink(src, (err, resolvedSrcPath) => {
+    if (err) return cb(err)
+
+    if (opts.dereference) {
+      resolvedSrcPath = path.resolve(process.cwd(), resolvedSrcPath)
     }
 
-    pathExists(dir, (err, dirExists) => {
-      if (err) return callback(err)
-      if (dirExists) return ncp(src, dest, options, callback)
-      mkdir.mkdirs(dir, err => {
-        if (err) return callback(err)
-        ncp(src, dest, options, callback)
-      })
+    checkDest(dest, (err, resolvedDestPath) => {
+      if (err) return cb(err)
+
+      if (resolvedDestPath === notExist || resolvedDestPath === existsReg) {
+        // if dest already exists, fs throws error anyway,
+        // so no need to guard against it here.
+        return fs.symlink(resolvedSrcPath, dest, cb)
+      } else {
+        if (opts.dereference) {
+          resolvedDestPath = path.resolve(process.cwd(), resolvedDestPath)
+        }
+        if (resolvedDestPath === resolvedSrcPath) return cb()
+
+        // prevent copy if src is a subdir of dest since unlinking
+        // dest in this case would result in removing src contents
+        // and therefore a broken symlink would be created.
+        fs.stat(dest, (err, st) => {
+          if (err) return cb(err)
+          if (st.isDirectory() && isSrcSubdir(resolvedDestPath, resolvedSrcPath)) {
+            return cb(new Error(`Cannot overwrite '${resolvedDestPath}' with '${resolvedSrcPath}'.`))
+          }
+          return copyLink(resolvedSrcPath, dest, cb)
+        })
+      }
     })
   })
 }
 
+function copyLink (resolvedSrcPath, dest, cb) {
+  fs.unlink(dest, err => {
+    if (err) return cb(err)
+    return fs.symlink(resolvedSrcPath, dest, cb)
+  })
+}
+
+// check if dest exists and/or is a symlink
+function checkDest (dest, cb) {
+  fs.readlink(dest, (err, resolvedPath) => {
+    if (err) {
+      if (err.code === 'ENOENT') return cb(null, notExist)
+
+      // dest exists and is a regular file or directory, Windows may throw UNKNOWN error.
+      if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return cb(null, existsReg)
+
+      return cb(err)
+    }
+    return cb(null, resolvedPath) // dest exists and is a symlink
+  })
+}
+
+// return true if dest is a subdir of src, otherwise false.
+// extract dest base dir and check if that is the same as src basename
+function isSrcSubdir (src, dest) {
+  const baseDir = dest.split(path.dirname(src) + path.sep)[1]
+  if (baseDir) {
+    const destBasename = baseDir.split(path.sep)[0]
+    if (destBasename) {
+      return src !== dest && dest.indexOf(src) > -1 && destBasename === path.basename(src)
+    }
+    return false
+  }
+  return false
+}
+
 module.exports = copy
diff --git a/lib/copy/ncp.js b/lib/copy/ncp.js
deleted file mode 100644
index 9670ee0..0000000
--- 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 eeeb30f..a718135 100644
--- a/lib/move/index.js
+++ b/lib/move/index.js
@@ -8,7 +8,7 @@
 
 const u = require('universalify').fromCallback
 const fs = require('graceful-fs')
-const ncp = require('../copy/ncp')
+const copy = require('../copy/copy')
 const path = require('path')
 const remove = require('../remove').remove
 const mkdirp = require('../mkdirs').mkdirs
@@ -133,14 +133,14 @@ function moveDirAcrossDevice (src, dest, overwrite, callback) {
   if (overwrite) {
     remove(dest, err => {
       if (err) return callback(err)
-      startNcp()
+      startCopy()
     })
   } else {
-    startNcp()
+    startCopy()
   }
 
-  function startNcp () {
-    ncp(src, dest, options, err => {
+  function startCopy () {
+    copy(src, dest, options, err => {
       if (err) return callback(err)
       remove(src, callback)
     })
diff --git a/lib/util/utimes.js b/lib/util/utimes.js
index 4c32099..8916a1b 100644
--- a/lib/util/utimes.js
+++ b/lib/util/utimes.js
@@ -64,9 +64,16 @@ function utimesMillis (path, atime, mtime, callback) {
   })
 }
 
+function utimesMillisSync (path, atime, mtime) {
+  const fd = fs.openSync(path, 'r+')
+  fs.futimesSync(fd, atime, mtime)
+  return fs.closeSync(fd)
+}
+
 module.exports = {
   hasMillisRes,
   hasMillisResSync,
   timeRemoveMillis,
-  utimesMillis
+  utimesMillis,
+  utimesMillisSync
 }
diff --git a/package.json b/package.json
index 8ceeb74..0a6dd77 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "fs-extra",
-  "version": "4.0.3",
+  "version": "5.0.0",
   "description": "fs-extra contains methods that aren't included in the vanilla Node.js fs package. Such as mkdir -p, cp -r, and rm -rf.",
   "homepage": "https://github.com/jprichardson/node-fs-extra",
   "repository": {

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-fs-extra.git



More information about the Pkg-javascript-commits mailing list