You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

246 lines
7.8 KiB

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