copy.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. 'use strict'
  2. const fs = require('graceful-fs')
  3. const path = require('path')
  4. const mkdirp = require('../mkdirs').mkdirs
  5. const pathExists = require('../path-exists').pathExists
  6. const utimes = require('../util/utimes').utimesMillis
  7. const notExist = Symbol('notExist')
  8. const existsReg = Symbol('existsReg')
  9. function copy (src, dest, opts, cb) {
  10. if (typeof opts === 'function' && !cb) {
  11. cb = opts
  12. opts = {}
  13. } else if (typeof opts === 'function') {
  14. opts = {filter: opts}
  15. }
  16. cb = cb || function () {}
  17. opts = opts || {}
  18. opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
  19. opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
  20. // Warn about using preserveTimestamps on 32-bit node
  21. if (opts.preserveTimestamps && process.arch === 'ia32') {
  22. console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n
  23. see https://github.com/jprichardson/node-fs-extra/issues/269`)
  24. }
  25. checkPaths(src, dest, (err, resolvedDest) => {
  26. if (err) return cb(err)
  27. if (opts.filter) return handleFilter(checkParentDir, resolvedDest, src, dest, opts, cb)
  28. return checkParentDir(resolvedDest, src, dest, opts, cb)
  29. })
  30. }
  31. function checkParentDir (resolvedDest, src, dest, opts, cb) {
  32. const destParent = path.dirname(dest)
  33. pathExists(destParent, (err, dirExists) => {
  34. if (err) return cb(err)
  35. if (dirExists) return startCopy(resolvedDest, src, dest, opts, cb)
  36. mkdirp(destParent, err => {
  37. if (err) return cb(err)
  38. return startCopy(resolvedDest, src, dest, opts, cb)
  39. })
  40. })
  41. }
  42. function startCopy (resolvedDest, src, dest, opts, cb) {
  43. if (opts.filter) return handleFilter(getStats, resolvedDest, src, dest, opts, cb)
  44. return getStats(resolvedDest, src, dest, opts, cb)
  45. }
  46. function handleFilter (onInclude, resolvedDest, src, dest, opts, cb) {
  47. Promise.resolve(opts.filter(src, dest)).then(include => {
  48. if (include) {
  49. if (resolvedDest) return onInclude(resolvedDest, src, dest, opts, cb)
  50. return onInclude(src, dest, opts, cb)
  51. }
  52. return cb()
  53. }, error => cb(error))
  54. }
  55. function getStats (resolvedDest, src, dest, opts, cb) {
  56. const stat = opts.dereference ? fs.stat : fs.lstat
  57. stat(src, (err, st) => {
  58. if (err) return cb(err)
  59. if (st.isDirectory()) return onDir(st, resolvedDest, src, dest, opts, cb)
  60. else if (st.isFile() ||
  61. st.isCharacterDevice() ||
  62. st.isBlockDevice()) return onFile(st, resolvedDest, src, dest, opts, cb)
  63. else if (st.isSymbolicLink()) return onLink(resolvedDest, src, dest, opts, cb)
  64. })
  65. }
  66. function onFile (srcStat, resolvedDest, src, dest, opts, cb) {
  67. if (resolvedDest === notExist) return copyFile(srcStat, src, dest, opts, cb)
  68. else if (resolvedDest === existsReg) return mayCopyFile(srcStat, src, dest, opts, cb)
  69. return mayCopyFile(srcStat, src, dest, opts, cb)
  70. }
  71. function mayCopyFile (srcStat, src, dest, opts, cb) {
  72. if (opts.overwrite) {
  73. fs.unlink(dest, err => {
  74. if (err) return cb(err)
  75. return copyFile(srcStat, src, dest, opts, cb)
  76. })
  77. } else if (opts.errorOnExist) {
  78. return cb(new Error(`'${dest}' already exists`))
  79. } else return cb()
  80. }
  81. function copyFile (srcStat, src, dest, opts, cb) {
  82. if (typeof fs.copyFile === 'function') {
  83. return fs.copyFile(src, dest, err => {
  84. if (err) return cb(err)
  85. return setDestModeAndTimestamps(srcStat, dest, opts, cb)
  86. })
  87. }
  88. return copyFileFallback(srcStat, src, dest, opts, cb)
  89. }
  90. function copyFileFallback (srcStat, src, dest, opts, cb) {
  91. const rs = fs.createReadStream(src)
  92. rs.on('error', err => cb(err)).once('open', () => {
  93. const ws = fs.createWriteStream(dest, { mode: srcStat.mode })
  94. ws.on('error', err => cb(err))
  95. .on('open', () => rs.pipe(ws))
  96. .once('close', () => setDestModeAndTimestamps(srcStat, dest, opts, cb))
  97. })
  98. }
  99. function setDestModeAndTimestamps (srcStat, dest, opts, cb) {
  100. fs.chmod(dest, srcStat.mode, err => {
  101. if (err) return cb(err)
  102. if (opts.preserveTimestamps) {
  103. return utimes(dest, srcStat.atime, srcStat.mtime, cb)
  104. }
  105. return cb()
  106. })
  107. }
  108. function onDir (srcStat, resolvedDest, src, dest, opts, cb) {
  109. if (resolvedDest === notExist) {
  110. if (isSrcSubdir(src, dest)) {
  111. return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
  112. }
  113. return mkDirAndCopy(srcStat, src, dest, opts, cb)
  114. } else if (resolvedDest === existsReg) {
  115. if (isSrcSubdir(src, dest)) {
  116. return cb(new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`))
  117. }
  118. return mayCopyDir(src, dest, opts, cb)
  119. }
  120. return copyDir(src, dest, opts, cb)
  121. }
  122. function mayCopyDir (src, dest, opts, cb) {
  123. fs.stat(dest, (err, st) => {
  124. if (err) return cb(err)
  125. if (!st.isDirectory()) {
  126. return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`))
  127. }
  128. return copyDir(src, dest, opts, cb)
  129. })
  130. }
  131. function mkDirAndCopy (srcStat, src, dest, opts, cb) {
  132. fs.mkdir(dest, srcStat.mode, err => {
  133. if (err) return cb(err)
  134. fs.chmod(dest, srcStat.mode, err => {
  135. if (err) return cb(err)
  136. return copyDir(src, dest, opts, cb)
  137. })
  138. })
  139. }
  140. function copyDir (src, dest, opts, cb) {
  141. fs.readdir(src, (err, items) => {
  142. if (err) return cb(err)
  143. return copyDirItems(items, src, dest, opts, cb)
  144. })
  145. }
  146. function copyDirItems (items, src, dest, opts, cb) {
  147. const item = items.pop()
  148. if (!item) return cb()
  149. return copyDirItem(items, item, src, dest, opts, cb)
  150. }
  151. function copyDirItem (items, item, src, dest, opts, cb) {
  152. const srcItem = path.join(src, item)
  153. const destItem = path.join(dest, item)
  154. checkPaths(srcItem, destItem, (err, resolvedDest) => {
  155. if (err) return cb(err)
  156. startCopy(resolvedDest, srcItem, destItem, opts, err => {
  157. if (err) return cb(err)
  158. return copyDirItems(items, src, dest, opts, cb)
  159. })
  160. })
  161. }
  162. function onLink (resolvedDest, src, dest, opts, cb) {
  163. fs.readlink(src, (err, resolvedSrc) => {
  164. if (err) return cb(err)
  165. if (opts.dereference) {
  166. resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
  167. }
  168. if (resolvedDest === notExist || resolvedDest === existsReg) {
  169. // if dest already exists, fs throws error anyway,
  170. // so no need to guard against it here.
  171. return fs.symlink(resolvedSrc, dest, cb)
  172. } else {
  173. if (opts.dereference) {
  174. resolvedDest = path.resolve(process.cwd(), resolvedDest)
  175. }
  176. if (pathsAreIdentical(resolvedSrc, resolvedDest)) return cb()
  177. // prevent copy if src is a subdir of dest since unlinking
  178. // dest in this case would result in removing src contents
  179. // and therefore a broken symlink would be created.
  180. fs.stat(dest, (err, st) => {
  181. if (err) return cb(err)
  182. if (st.isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
  183. return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`))
  184. }
  185. return copyLink(resolvedSrc, dest, cb)
  186. })
  187. }
  188. })
  189. }
  190. function copyLink (resolvedSrc, dest, cb) {
  191. fs.unlink(dest, err => {
  192. if (err) return cb(err)
  193. return fs.symlink(resolvedSrc, dest, cb)
  194. })
  195. }
  196. // return true if dest is a subdir of src, otherwise false.
  197. // extract dest base dir and check if that is the same as src basename.
  198. function isSrcSubdir (src, dest) {
  199. const srcArray = path.resolve(src).split(path.sep)
  200. const destArray = path.resolve(dest).split(path.sep)
  201. return srcArray.reduce((acc, current, i) => {
  202. return acc && destArray[i] === current
  203. }, true)
  204. }
  205. // check if dest exists and is a symlink.
  206. function checkDest (dest, cb) {
  207. fs.readlink(dest, (err, resolvedPath) => {
  208. if (err) {
  209. if (err.code === 'ENOENT') return cb(null, notExist)
  210. // dest exists and is a regular file or directory, Windows may throw UNKNOWN error.
  211. if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return cb(null, existsReg)
  212. return cb(err)
  213. }
  214. return cb(null, resolvedPath) // dest exists and is a symlink
  215. })
  216. }
  217. function pathsAreIdentical (src, dest) {
  218. const os = process.platform
  219. const resolvedSrc = path.resolve(src)
  220. const resolvedDest = path.resolve(dest)
  221. // case-insensitive paths
  222. if (os === 'darwin' || os === 'win32') {
  223. return resolvedSrc.toLowerCase() === resolvedDest.toLowerCase()
  224. }
  225. return resolvedSrc === resolvedDest
  226. }
  227. function checkPaths (src, dest, cb) {
  228. checkDest(dest, (err, resolvedDest) => {
  229. if (err) return cb(err)
  230. if (resolvedDest === notExist || resolvedDest === existsReg) {
  231. if (pathsAreIdentical(src, dest)) return cb(new Error('Source and destination must not be the same.'))
  232. return cb(null, resolvedDest)
  233. } else {
  234. // check resolved dest path if dest is a symlink
  235. if (pathsAreIdentical(src, resolvedDest)) return cb(new Error('Source and destination must not be the same.'))
  236. return cb(null, resolvedDest)
  237. }
  238. })
  239. }
  240. module.exports = copy