Repository: jprichardson/node-fs-extra (js)

Issue:
Prevent copying a directory into itself
`fse.copy('src','src/dest')`  will create the dest  continues to recurse, and finally it throws an error `ENAMETOOLONG`

It links to AvianFlu/ncp#4


Pull request:
Rewrite copy
Started #374.

Fixes #83 and #198.

This implementation uses `recursive` pattern for copying directories. `recursive` pattern has shown better performance than `Promise` ([copy-benchmark](https://github.com/manidlou/benchmark-fs-extra-copy)). If this looks good, then we will apply `fs.copyFile()` to it.

Any feedback appreciated! :smile: 

Patch:
diff --git a/lib/copy/copy.js b/lib/copy/copy.js
index 309a93d..1aa5322 100644
--- a/lib/copy/copy.js
+++ b/lib/copy/copy.js
@@ -2,53 +2,246 @@
 
 const fs = require('graceful-fs')
 const path = require('path')
-const ncp = require('./ncp')
-const mkdir = require('../mkdirs')
+const mkdirp = require('../mkdirs').mkdirs
 const pathExists = require('../path-exists').pathExists
+const utimes = require('../util/utimes').utimesMillis
 
-function copy (src, dest, options, callback) {
-  if (typeof options === 'function' && !callback) {
-    callback = options
-    options = {}
-  } else if (typeof options === 'function' || options instanceof RegExp) {
-    options = {filter: options}
+const notExist = Symbol('notExist')
+const existsReg = Symbol('existsReg')
+
+function copy (src, dest, opts, cb) {
+  if (typeof opts === 'function' && !cb) {
+    cb = opts
+    opts = {}
+  } else if (typeof opts === 'function' || opts instanceof RegExp) {
+    opts = {filter: opts}
   }
-  callback = callback || function () {}
-  options = options || {}
 
-  // Warn about using preserveTimestamps on 32-bit node:
-  if (options.preserveTimestamps && process.arch === 'ia32') {
+  cb = cb || function () {}
+  opts = opts || {}
+
+  opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
+  opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
+
+  // Warn about using preserveTimestamps on 32-bit node
+  if (opts.preserveTimestamps && process.arch === 'ia32') {
     console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n
     see https://github.com/jprichardson/node-fs-extra/issues/269`)
   }
 
+  src = path.resolve(src)
+  dest = path.resolve(dest)
+
   // don't allow src and dest to be the same
-  const basePath = process.cwd()
-  const currentPath = path.resolve(basePath, src)
-  const targetPath = path.resolve(basePath, dest)
-  if (currentPath === targetPath) return callback(new Error('Source and destination must not be the same.'))
-
-  fs.lstat(src, (err, stats) => {
-    if (err) return callback(err)
-
-    let dir = null
-    if (stats.isDirectory()) {
-      const parts = dest.split(path.sep)
-      parts.pop()
-      dir = parts.join(path.sep)
+  if (src === dest) return cb(new Error('Source and destination must not be the same.'))
+
+  const destParent = path.dirname(dest)
+  pathExists(destParent, (err, dirExists) => {
+    if (err) return cb(err)
+    if (dirExists) return startCopy(src, dest, opts, cb)
+    mkdirp(destParent, err => {
+      if (err) return cb(err)
+      return startCopy(src, dest, opts, cb)
+    })
+  })
+}
+
+function startCopy (src, dest, opts, cb) {
+  if (opts.filter) {
+    if (opts.filter instanceof RegExp) {
+      console.warn('Warning: fs-extra: Passing a RegExp filter is deprecated, use a function')
+      if (!opts.filter.test(src)) return cb()
+    } else if (typeof opts.filter === 'function') {
+      if (!opts.filter(src, dest)) return cb()
+    }
+  }
+  return getStats(src, dest, opts, cb)
+}
+
+function getStats (src, dest, opts, cb) {
+  const stat = opts.dereference ? fs.stat : fs.lstat
+  stat(src, (err, st) => {
+    if (err) return cb(err)
+
+    if (st.isDirectory()) return onDir(st, src, dest, opts, cb)
+    else if (st.isFile() ||
+             st.isCharacterDevice() ||
+             st.isBlockDevice()) return onFile(st, src, dest, opts, cb)
+    else if (st.isSymbolicLink()) return onLink(src, dest, opts, cb)
+  })
+}
+
+function onFile (srcStat, src, dest, opts, cb) {
+  checkDest(dest, (err, resolvedPath) => {
+    if (err) return cb(err)
+    if (resolvedPath === notExist) {
+      return cpFile(srcStat, src, dest, opts, cb)
+    } else if (resolvedPath === existsReg) {
+      return mayCopyFile(srcStat, src, dest, opts, cb)
     } else {
-      dir = path.dirname(dest)
+      if (src === resolvedPath) return cb()
+      return mayCopyFile(srcStat, src, dest, opts, cb)
     }
+  })
+}
+
+function mayCopyFile (srcStat, src, dest, opts, cb) {
+  if (opts.overwrite) {
+    fs.unlink(dest, err => {
+      if (err) return cb(err)
+      return cpFile(srcStat, src, dest, opts, cb)
+    })
+  } else if (opts.errorOnExist) {
+    return cb(new Error(`'${dest}' already exists`))
+  } else return cb()
+}
+
+function cpFile (srcStat, src, dest, opts, cb) {
+  const rs = fs.createReadStream(src)
+  const ws = fs.createWriteStream(dest, { mode: srcStat.mode })
 
-    pathExists(dir, (err, dirExists) => {
-      if (err) return callback(err)
-      if (dirExists) return ncp(src, dest, options, callback)
-      mkdir.mkdirs(dir, err => {
-        if (err) return callback(err)
-        ncp(src, dest, options, callback)
-      })
+  rs.on('error', err => cb(err))
+  ws.on('error', err => cb(err))
+
+  ws.on('open', () => {
+    rs.pipe(ws)
+  }).once('close', () => {
+    fs.chmod(dest, srcStat.mode, err => {
+      if (err) return cb(err)
+      if (opts.preserveTimestamps) {
+        return utimes(dest, srcStat.atime, srcStat.mtime, cb)
+      }
+      return cb()
     })
   })
 }
 
+function onDir (srcStat, src, dest, opts, cb) {
+  checkDest(dest, (err, resolvedPath) => {
+    if (err) return cb(err)
+    if (resolvedPath === notExist) {
+      if (isSrcSubdir(src, dest)) {
+        return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
+      }
+      return mkDirAndCopy(srcStat, src, dest, opts, cb)
+    } else if (resolvedPath === existsReg) {
+      if (isSrcSubdir(src, dest)) {
+        return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
+      }
+      return mayCopyDir(src, dest, opts, cb)
+    } else {
+      if (src === resolvedPath) return cb()
+      return cpDir(src, dest, opts, cb)
+    }
+  })
+}
+
+function mayCopyDir (src, dest, opts, cb) {
+  fs.stat(dest, (err, st) => {
+    if (err) return cb(err)
+    if (!st.isDirectory()) {
+      return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`))
+    }
+    return cpDir(src, dest, opts, cb)
+  })
+}
+
+function mkDirAndCopy (srcStat, src, dest, opts, cb) {
+  fs.mkdir(dest, srcStat.mode, err => {
+    if (err) return cb(err)
+    fs.chmod(dest, srcStat.mode, err => {
+      if (err) return cb(err)
+      return cpDir(src, dest, opts, cb)
+    })
+  })
+}
+
+function cpDir (src, dest, opts, cb) {
+  fs.readdir(src, (err, items) => {
+    if (err) return cb(err)
+    return cpDirItems(items, src, dest, opts, cb)
+  })
+}
+
+function cpDirItems (items, src, dest, opts, cb) {
+  const item = items.pop()
+  if (!item) return cb()
+  startCopy(path.join(src, item), path.join(dest, item), opts, err => {
+    if (err) return cb(err)
+    return cpDirItems(items, src, dest, opts, cb)
+  })
+}
+
+function onLink (src, dest, opts, cb) {
+  fs.readlink(src, (err, resolvedSrcPath) => {
+    if (err) return cb(err)
+
+    if (opts.dereference) {
+      resolvedSrcPath = path.resolve(process.cwd(), resolvedSrcPath)
+    }
+
+    checkDest(dest, (err, resolvedDestPath) => {
+      if (err) return cb(err)
+
+      if (resolvedDestPath === notExist || resolvedDestPath === existsReg) {
+        // if dest already exists, fs throws error anyway,
+        // so no need to guard against it here.
+        return fs.symlink(resolvedSrcPath, dest, cb)
+      } else {
+        if (opts.dereference) {
+          resolvedDestPath = path.resolve(process.cwd(), resolvedDestPath)
+        }
+        if (resolvedDestPath === resolvedSrcPath) return cb()
+
+        // prevent copy if src is a subdir of dest since unlinking
+        // dest in this case results in removing src contents
+        // and therefore a broken symlink will be created.
+        fs.stat(dest, (err, st) => {
+          if (err) return cb(err)
+          if (st.isDirectory() && isSrcSubdir(resolvedDestPath, resolvedSrcPath)) {
+            return cb(new Error(`Cannot overwrite '${resolvedDestPath}' with '${resolvedSrcPath}'.`))
+          }
+          return cpLink(resolvedSrcPath, dest, cb)
+        })
+      }
+    })
+  })
+}
+
+function cpLink (resolvedSrcPath, dest, cb) {
+  fs.unlink(dest, err => {
+    if (err) return cb(err)
+    return fs.symlink(resolvedSrcPath, dest, cb)
+  })
+}
+
+// check dest to see if it exists and/or is a symlink
+function checkDest (dest, cb) {
+  fs.readlink(dest, (err, resolvedPath) => {
+    if (err) {
+      if (err.code === 'ENOENT') return cb(null, notExist)
+
+      // dest exists and is a regular file or directory, Windows throws UNKNOWN error.
+      if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return cb(null, existsReg)
+
+      return cb(err)
+    }
+    return cb(null, resolvedPath) // dest exists and is a symlink
+  })
+}
+
+// return true if dest is a subdir of src, otherwise false.
+// extract dest base dir and check if that is the same as src basename
+function isSrcSubdir (src, dest) {
+  const baseDir = dest.split(path.dirname(src) + path.sep)[1]
+  if (baseDir) {
+    const destBasename = baseDir.split(path.sep)[0]
+    if (destBasename) {
+      return src !== dest && dest.indexOf(src) > -1 && destBasename === path.basename(src)
+    }
+    return false
+  }
+  return false
+}
+
 module.exports = copy
diff --git a/lib/copy/ncp.js b/lib/copy/ncp.js
deleted file mode 100644
index 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)
     })


Test patch:
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..6435710
--- /dev/null
+++ b/lib/copy/__tests__/copy-prevent-copying-identical.test.js
@@ -0,0 +1,192 @@
+'use strict'
+
+const assert = require('assert')
+const os = require('os')
+const path = require('path')
+const fs = require(process.cwd())
+const klawSync = require('klaw-sync')
+
+/* global beforeEach, afterEach, describe, it */
+
+describe('+ copySync() - prevent copying identical files and dirs', () => {
+  let TEST_DIR = ''
+  let src = ''
+  let dest = ''
+
+  beforeEach(done => {
+    TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-sync-prevent-copying-identical')
+    fs.emptyDir(TEST_DIR, done)
+  })
+
+  afterEach(done => fs.remove(TEST_DIR, done))
+
+  it('should return an error if src and dest are the same', done => {
+    const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_copy_sync')
+    const fileDest = path.join(TEST_DIR, 'TEST_fs-extra_copy_sync')
+
+    fs.copy(fileSrc, fileDest, err => {
+      assert.equal(err.message, 'Source and destination must not be the same.')
+      done()
+    })
+  })
+
+  // src is directory:
+  //  src is regular, dest is symlink
+  //  src is symlink, dest is regular
+  //  src is symlink, dest is symlink
+
+  describe('> when the source is a directory', () => {
+    describe(`>> when src is regular and dest is a symlink that points to src`, () => {
+      it('should not copy and return', done => {
+        src = path.join(TEST_DIR, 'src')
+        fs.mkdirsSync(src)
+        const subdir = path.join(TEST_DIR, 'src', 'subdir')
+        fs.mkdirsSync(subdir)
+        fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data')
+
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const oldlen = klawSync(src).length
+
+        fs.copy(src, destLink, err => {
+          assert.ifError(err)
+
+          const newlen = klawSync(src).length
+          assert.strictEqual(newlen, oldlen)
+          const link = fs.readlinkSync(destLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+    })
+
+    describe(`>> when src is a symlink that points to a regular dest`, () => {
+      it('should throw error', done => {
+        dest = path.join(TEST_DIR, 'dest')
+        fs.mkdirsSync(dest)
+        const subdir = path.join(TEST_DIR, 'dest', 'subdir')
+        fs.mkdirsSync(subdir)
+        fs.writeFileSync(path.join(subdir, 'file.txt'), 'some data')
+
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(dest, srcLink, 'dir')
+
+        const oldlen = klawSync(dest).length
+
+        fs.copy(srcLink, dest, err => {
+          assert.ok(err)
+
+          // assert nothing copied
+          const newlen = klawSync(dest).length
+          assert.strictEqual(newlen, oldlen)
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, dest)
+          done()
+        })
+      })
+    })
+
+    describe('>> when src and dest are symlinks that point to the exact same path', () => {
+      it('should not copy and return', done => {
+        src = path.join(TEST_DIR, 'src')
+        fs.mkdirsSync(src)
+        const srcLink = path.join(TEST_DIR, 'src_symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+        const destLink = path.join(TEST_DIR, 'dest_symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const srclenBefore = klawSync(srcLink).length
+        const destlenBefore = klawSync(destLink).length
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+
+          const srclenAfter = klawSync(srcLink).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+          const destlenAfter = klawSync(destLink).length
+          assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change')
+
+          const srcln = fs.readlinkSync(srcLink)
+          assert.strictEqual(srcln, src)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          done()
+        })
+      })
+    })
+  })
+
+  // src is file:
+  //  src is regular, dest is symlink
+  //  src is symlink, dest is regular
+  //  src is symlink, dest is symlink
+
+  describe('> when the source is a file', () => {
+    describe(`>> when src is regular and dest is a symlink that points to src`, () => {
+      it('should not copy and return', done => {
+        src = path.join(TEST_DIR, 'src', 'somefile.txt')
+        fs.ensureFileSync(src)
+        fs.writeFileSync(src, 'some data')
+
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'file')
+
+        fs.copy(src, destLink, err => {
+          assert.ifError(err)
+
+          const link = fs.readlinkSync(destLink)
+          assert.strictEqual(link, src)
+          assert(fs.readFileSync(link, 'utf8'), 'some data')
+          done()
+        })
+      })
+    })
+
+    describe(`>> when src is a symlink that points to a regular dest`, () => {
+      it('should throw error', done => {
+        dest = path.join(TEST_DIR, 'dest', 'somefile.txt')
+        fs.ensureFileSync(dest)
+        fs.writeFileSync(dest, 'some data')
+
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(dest, srcLink, 'file')
+
+        fs.copy(srcLink, dest, err => {
+          assert.ok(err)
+
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, dest)
+          assert(fs.readFileSync(link, 'utf8'), 'some data')
+          done()
+        })
+      })
+    })
+
+    describe('>> when src and dest are symlinks that point to the exact same path', () => {
+      it('should not copy and return', done => {
+        src = path.join(TEST_DIR, 'src', 'srcfile.txt')
+        fs.ensureFileSync(src)
+        fs.writeFileSync(src, 'src data')
+
+        const srcLink = path.join(TEST_DIR, 'src_symlink')
+        fs.symlinkSync(src, srcLink, 'file')
+
+        const destLink = path.join(TEST_DIR, 'dest_symlink')
+        fs.symlinkSync(src, destLink, 'file')
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+
+          const srcln = fs.readlinkSync(srcLink)
+          assert.strictEqual(srcln, src)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          assert(fs.readFileSync(srcln, 'utf8'), 'src data')
+          assert(fs.readFileSync(destln, 'utf8'), 'src data')
+          done()
+        })
+      })
+    })
+  })
+})
diff --git a/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js
new file mode 100644
index 0000000..d759a1a
--- /dev/null
+++ b/lib/copy/__tests__/copy-prevent-copying-into-itself.test.js
@@ -0,0 +1,372 @@
+'use strict'
+
+const assert = require('assert')
+const os = require('os')
+const path = require('path')
+const fs = require(process.cwd())
+const klawSync = require('klaw-sync')
+
+/* global beforeEach, afterEach, describe, it */
+
+// these files are used for all tests
+const FILES = [
+  'file0.txt',
+  path.join('dir1', 'file1.txt'),
+  path.join('dir1', 'dir2', 'file2.txt'),
+  path.join('dir1', 'dir2', 'dir3', 'file3.txt')
+]
+
+const dat0 = 'file0'
+const dat1 = 'file1'
+const dat2 = 'file2'
+const dat3 = 'file3'
+
+function testSuccess (src, dest, done) {
+  const srclen = klawSync(src).length
+  assert(srclen > 2)
+  fs.copy(src, dest, err => {
+    assert.ifError(err)
+
+    const destlen = klawSync(dest).length
+
+    assert.strictEqual(destlen, srclen)
+
+    FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied'))
+
+    const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8')
+    const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8')
+    const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8')
+    const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8')
+
+    assert.strictEqual(o0, dat0, 'file contents matched')
+    assert.strictEqual(o1, dat1, 'file contents matched')
+    assert.strictEqual(o2, dat2, 'file contents matched')
+    assert.strictEqual(o3, dat3, 'file contents matched')
+    done()
+  })
+}
+
+function testError (src, dest, done) {
+  fs.copy(src, dest, err => {
+    assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
+    done()
+  })
+}
+
+describe('+ copy() - prevent copying into itself', () => {
+  let TEST_DIR, src
+
+  beforeEach(done => {
+    TEST_DIR = path.join(os.tmpdir(), 'fs-extra', 'copy-prevent-copying-into-itself-4')
+    src = path.join(TEST_DIR, 'src')
+    fs.mkdirpSync(src)
+
+    fs.outputFileSync(path.join(src, FILES[0]), dat0)
+    fs.outputFileSync(path.join(src, FILES[1]), dat1)
+    fs.outputFileSync(path.join(src, FILES[2]), dat2)
+    fs.outputFileSync(path.join(src, FILES[3]), dat3)
+    done()
+  })
+
+  afterEach(done => fs.remove(TEST_DIR, done))
+
+  describe('> when source is a file', () => {
+    it('should copy the file successfully even when dest parent is a subdir of src', done => {
+      const srcFile = path.join(TEST_DIR, 'src', 'srcfile.txt')
+      const destFile = path.join(TEST_DIR, 'src', 'dest', 'destfile.txt')
+      fs.writeFileSync(srcFile, dat0)
+
+      fs.copy(srcFile, destFile, err => {
+        assert.ifError(err)
+
+        assert(fs.existsSync(destFile, 'file copied'))
+        const out = fs.readFileSync(destFile, 'utf8')
+        assert.strictEqual(out, dat0, 'file contents matched')
+        done()
+      })
+    })
+  })
+
+  // test for these cases:
+  //  - src is directory, dest is directory
+  //  - src is directory, dest is symlink
+  //  - src is symlink, dest is directory
+  //  - src is symlink, dest is symlink
+
+  describe('> when source is a directory', () => {
+    describe('>> when dest is a directory', () => {
+      it(`should copy the directory successfully when dest is 'src_dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src_dest')
+        return testSuccess(src, dest, done)
+      })
+      it(`should copy the directory successfully when dest is 'src-dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src-dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest_src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest_src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src_dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src_dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src-dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src-dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest_src/src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest_src', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src_src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src_src', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'src-src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src-src', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'srcsrc', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should copy the directory successfully when dest is 'dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'dest', 'src')
+        return testSuccess(src, dest, done)
+      })
+
+      it('should copy the directory successfully when dest is very nested that all its parents need to be created', done => {
+        const dest = path.join(TEST_DIR, 'dest', 'src', 'foo', 'bar', 'baz', 'qux', 'quux', 'waldo',
+          'grault', 'garply', 'fred', 'plugh', 'thud', 'some', 'highly', 'deeply',
+          'badly', 'nasty', 'crazy', 'mad', 'nested', 'dest')
+        return testSuccess(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/src_dest'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'src_dest')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest_src'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest_src')
+        return testError(src, dest, done)
+      })
+
+      it(`should error when dest is 'src/dest/src'`, done => {
+        const dest = path.join(TEST_DIR, 'src', 'dest', 'src')
+        return testError(src, dest, done)
+      })
+    })
+
+    describe('>> when dest is a symlink', () => {
+      it('should not copy and return when dest points exactly to src', done => {
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const srclenBefore = klawSync(src).length
+        assert(srclenBefore > 2)
+
+        fs.copy(src, destLink, err => {
+          assert.ifError(err)
+
+          const srclenAfter = klawSync(src).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+
+          const link = fs.readlinkSync(destLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should copy the directory successfully when src is a subdir of resolved dest path', done => {
+        const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.copySync(src, srcInDest) // put some stuff in srcInDest
+
+        const dest = path.join(TEST_DIR, 'dest')
+        fs.symlinkSync(dest, destLink, 'dir')
+
+        const srclen = klawSync(srcInDest).length
+        const destlenBefore = klawSync(dest).length
+
+        assert(srclen > 2)
+        fs.copy(srcInDest, destLink, err => {
+          assert.ifError(err)
+
+          const destlenAfter = klawSync(dest).length
+
+          // assert dest length is oldlen + length of stuff copied from src
+          assert.strictEqual(destlenAfter, destlenBefore + srclen, 'dest length should be equal to old length + copied legnth')
+
+          FILES.forEach(f => assert(fs.existsSync(path.join(dest, f)), 'file copied'))
+
+          const o0 = fs.readFileSync(path.join(dest, FILES[0]), 'utf8')
+          const o1 = fs.readFileSync(path.join(dest, FILES[1]), 'utf8')
+          const o2 = fs.readFileSync(path.join(dest, FILES[2]), 'utf8')
+          const o3 = fs.readFileSync(path.join(dest, FILES[3]), 'utf8')
+
+          assert.strictEqual(o0, dat0, 'files contents matched')
+          assert.strictEqual(o1, dat1, 'files contents matched')
+          assert.strictEqual(o2, dat2, 'files contents matched')
+          assert.strictEqual(o3, dat3, 'files contents matched')
+          done()
+        })
+      })
+    })
+  })
+
+  describe('> when source is a symlink', () => {
+    describe('>> when dest is a directory', () => {
+      it('should error when resolved src path points to dest', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src')
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          // assert source not affected
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should error when dest is a subdir of resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
+        fs.mkdirsSync(dest)
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          const link = fs.readlinkSync(srcLink)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it('should error when resolved src path is a subdir of dest', done => {
+        const dest = path.join(TEST_DIR, 'dest')
+
+        const resolvedSrcPath = path.join(dest, 'contains', 'src')
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.copySync(src, resolvedSrcPath)
+
+        // make symlink that points to a subdir in dest
+        fs.symlinkSync(resolvedSrcPath, srcLink, 'dir')
+
+        fs.copy(srcLink, dest, err => {
+          assert(err)
+          done()
+        })
+      })
+
+      it(`should copy the directory successfully when dest is 'src_src/dest'`, done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'src_src', 'dest')
+        testSuccess(srcLink, dest, () => {
+          const link = fs.readlinkSync(dest)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+
+      it(`should copy the directory successfully when dest is 'srcsrc/dest'`, done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const dest = path.join(TEST_DIR, 'srcsrc', 'dest')
+        testSuccess(srcLink, dest, () => {
+          const link = fs.readlinkSync(dest)
+          assert.strictEqual(link, src)
+          done()
+        })
+      })
+    })
+
+    describe('>> when dest is a symlink', () => {
+      it('should silently return when resolved dest path is exactly the same as resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        fs.symlinkSync(src, destLink, 'dir')
+
+        const srclenBefore = klawSync(srcLink).length
+        const destlenBefore = klawSync(destLink).length
+        assert(srclenBefore > 2)
+        assert(destlenBefore > 2)
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+
+          const srclenAfter = klawSync(srcLink).length
+          assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
+          const destlenAfter = klawSync(destLink).length
+          assert.strictEqual(destlenAfter, destlenBefore, 'dest length should not change')
+
+          const srcln = fs.readlinkSync(srcLink)
+          assert.strictEqual(srcln, src)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          done()
+        })
+      })
+
+      it('should error when resolved dest path is a subdir of resolved src path', done => {
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        fs.symlinkSync(src, srcLink, 'dir')
+
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+        const resolvedDestPath = path.join(TEST_DIR, 'src', 'some', 'nested', 'dest')
+        fs.ensureFileSync(path.join(resolvedDestPath, 'subdir', 'somefile.txt'))
+
+        fs.symlinkSync(resolvedDestPath, destLink, 'dir')
+
+        fs.copy(srcLink, destLink, err => {
+          assert.ifError(err)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, src)
+          done()
+        })
+      })
+
+      it('should error when resolved src path is a subdir of resolved dest path', done => {
+        const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
+        const srcLink = path.join(TEST_DIR, 'src-symlink')
+        const destLink = path.join(TEST_DIR, 'dest-symlink')
+
+        const dest = path.join(TEST_DIR, 'dest')
+
+        fs.mkdirSync(dest)
+
+        fs.symlinkSync(srcInDest, srcLink, 'dir')
+        fs.symlinkSync(dest, destLink, 'dir')
+
+        fs.copy(srcLink, destLink, err => {
+          assert.strictEqual(err.message, `Cannot overwrite '${dest}' with '${srcInDest}'.`)
+          const destln = fs.readlinkSync(destLink)
+          assert.strictEqual(destln, dest)
+          done()
+        })
+      })
+    })
+  })
+})
diff --git a/lib/copy/__tests__/copy.test.js b/lib/copy/__tests__/copy.test.js
index 6ed64ff..4787bfd 100644
--- a/lib/copy/__tests__/copy.test.js
+++ b/lib/copy/__tests__/copy.test.js
@@ -30,6 +30,30 @@ describe('fs-extra', () => {
       })
     })
 
+    it('should error when overwrite=false and file exists', done => {
+      const src = path.join(TEST_DIR, 'src.txt')
+      const dest = path.join(TEST_DIR, 'dest.txt')
+
+      fse.ensureFileSync(src)
+      fse.ensureFileSync(dest)
+      fse.copy(src, dest, {overwrite: false, errorOnExist: true}, err => {
+        assert(err)
+        done()
+      })
+    })
+
+    it('should error when overwrite=false and file exists in a dir', done => {
+      const src = path.join(TEST_DIR, 'src', 'sfile.txt')
+      const dest = path.join(TEST_DIR, 'dest', 'dfile.txt')
+
+      fse.ensureFileSync(src)
+      fse.ensureFileSync(dest)
+      fse.copy(src, dest, {overwrite: false, errorOnExist: true}, err => {
+        assert(err)
+        done()
+      })
+    })
+
     describe('> when the source is a file', () => {
       it('should copy the file asynchronously', done => {
         const fileSrc = path.join(TEST_DIR, 'TEST_fs-extra_src')
@@ -98,6 +122,20 @@ describe('fs-extra', () => {
         })
       })
 
+      describe('> when dest exists and is a file', () => {
+        it('should return an error', done => {
+          const src = path.join(TEST_DIR, 'src')
+          const dest = path.join(TEST_DIR, 'file.txt')
+          fs.mkdirSync(src)
+          fse.ensureFileSync(dest)
+
+          fse.copy(src, dest, err => {
+            assert.strictEqual(err.message, `Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
+            done()
+          })
+        })
+      })
+
       it('should copy the directory asynchronously', done => {
         const FILES = 2
         const src = path.join(TEST_DIR, 'src')
diff --git a/lib/copy/__tests__/ncp/broken-symlink.test.js b/lib/copy/__tests__/ncp/broken-symlink.test.js
index 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')