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.

193 lines
6.0 KiB

4 years ago
  1. 'use strict'
  2. const fs = require('graceful-fs')
  3. const path = require('path')
  4. const mkdirpSync = require('../mkdirs').mkdirsSync
  5. const utimesSync = require('../util/utimes.js').utimesMillisSync
  6. const notExist = Symbol('notExist')
  7. function copySync (src, dest, opts) {
  8. if (typeof opts === 'function') {
  9. opts = {filter: opts}
  10. }
  11. opts = opts || {}
  12. opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
  13. opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
  14. // Warn about using preserveTimestamps on 32-bit node
  15. if (opts.preserveTimestamps && process.arch === 'ia32') {
  16. console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n
  17. see https://github.com/jprichardson/node-fs-extra/issues/269`)
  18. }
  19. const destStat = checkPaths(src, dest)
  20. if (opts.filter && !opts.filter(src, dest)) return
  21. const destParent = path.dirname(dest)
  22. if (!fs.existsSync(destParent)) mkdirpSync(destParent)
  23. return startCopy(destStat, src, dest, opts)
  24. }
  25. function startCopy (destStat, src, dest, opts) {
  26. if (opts.filter && !opts.filter(src, dest)) return
  27. return getStats(destStat, src, dest, opts)
  28. }
  29. function getStats (destStat, src, dest, opts) {
  30. const statSync = opts.dereference ? fs.statSync : fs.lstatSync
  31. const srcStat = statSync(src)
  32. if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts)
  33. else if (srcStat.isFile() ||
  34. srcStat.isCharacterDevice() ||
  35. srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts)
  36. else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts)
  37. }
  38. function onFile (srcStat, destStat, src, dest, opts) {
  39. if (destStat === notExist) return copyFile(srcStat, src, dest, opts)
  40. return mayCopyFile(srcStat, src, dest, opts)
  41. }
  42. function mayCopyFile (srcStat, src, dest, opts) {
  43. if (opts.overwrite) {
  44. fs.unlinkSync(dest)
  45. return copyFile(srcStat, src, dest, opts)
  46. } else if (opts.errorOnExist) {
  47. throw new Error(`'${dest}' already exists`)
  48. }
  49. }
  50. function copyFile (srcStat, src, dest, opts) {
  51. if (typeof fs.copyFileSync === 'function') {
  52. fs.copyFileSync(src, dest)
  53. fs.chmodSync(dest, srcStat.mode)
  54. if (opts.preserveTimestamps) {
  55. return utimesSync(dest, srcStat.atime, srcStat.mtime)
  56. }
  57. return
  58. }
  59. return copyFileFallback(srcStat, src, dest, opts)
  60. }
  61. function copyFileFallback (srcStat, src, dest, opts) {
  62. const BUF_LENGTH = 64 * 1024
  63. const _buff = require('../util/buffer')(BUF_LENGTH)
  64. const fdr = fs.openSync(src, 'r')
  65. const fdw = fs.openSync(dest, 'w', srcStat.mode)
  66. let pos = 0
  67. while (pos < srcStat.size) {
  68. const bytesRead = fs.readSync(fdr, _buff, 0, BUF_LENGTH, pos)
  69. fs.writeSync(fdw, _buff, 0, bytesRead)
  70. pos += bytesRead
  71. }
  72. if (opts.preserveTimestamps) fs.futimesSync(fdw, srcStat.atime, srcStat.mtime)
  73. fs.closeSync(fdr)
  74. fs.closeSync(fdw)
  75. }
  76. function onDir (srcStat, destStat, src, dest, opts) {
  77. if (destStat === notExist) return mkDirAndCopy(srcStat, src, dest, opts)
  78. if (destStat && !destStat.isDirectory()) {
  79. throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
  80. }
  81. return copyDir(src, dest, opts)
  82. }
  83. function mkDirAndCopy (srcStat, src, dest, opts) {
  84. fs.mkdirSync(dest)
  85. copyDir(src, dest, opts)
  86. return fs.chmodSync(dest, srcStat.mode)
  87. }
  88. function copyDir (src, dest, opts) {
  89. fs.readdirSync(src).forEach(item => copyDirItem(item, src, dest, opts))
  90. }
  91. function copyDirItem (item, src, dest, opts) {
  92. const srcItem = path.join(src, item)
  93. const destItem = path.join(dest, item)
  94. const destStat = checkPaths(srcItem, destItem)
  95. return startCopy(destStat, srcItem, destItem, opts)
  96. }
  97. function onLink (destStat, src, dest, opts) {
  98. let resolvedSrc = fs.readlinkSync(src)
  99. if (opts.dereference) {
  100. resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
  101. }
  102. if (destStat === notExist) {
  103. return fs.symlinkSync(resolvedSrc, dest)
  104. } else {
  105. let resolvedDest
  106. try {
  107. resolvedDest = fs.readlinkSync(dest)
  108. } catch (err) {
  109. // dest exists and is a regular file or directory,
  110. // Windows may throw UNKNOWN error. If dest already exists,
  111. // fs throws error anyway, so no need to guard against it here.
  112. if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlinkSync(resolvedSrc, dest)
  113. throw err
  114. }
  115. if (opts.dereference) {
  116. resolvedDest = path.resolve(process.cwd(), resolvedDest)
  117. }
  118. if (isSrcSubdir(resolvedSrc, resolvedDest)) {
  119. throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)
  120. }
  121. // prevent copy if src is a subdir of dest since unlinking
  122. // dest in this case would result in removing src contents
  123. // and therefore a broken symlink would be created.
  124. if (fs.statSync(dest).isDirectory() && isSrcSubdir(resolvedDest, resolvedSrc)) {
  125. throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`)
  126. }
  127. return copyLink(resolvedSrc, dest)
  128. }
  129. }
  130. function copyLink (resolvedSrc, dest) {
  131. fs.unlinkSync(dest)
  132. return fs.symlinkSync(resolvedSrc, dest)
  133. }
  134. // return true if dest is a subdir of src, otherwise false.
  135. function isSrcSubdir (src, dest) {
  136. const srcArray = path.resolve(src).split(path.sep)
  137. const destArray = path.resolve(dest).split(path.sep)
  138. return srcArray.reduce((acc, current, i) => acc && destArray[i] === current, true)
  139. }
  140. function checkStats (src, dest) {
  141. const srcStat = fs.statSync(src)
  142. let destStat
  143. try {
  144. destStat = fs.statSync(dest)
  145. } catch (err) {
  146. if (err.code === 'ENOENT') return {srcStat, destStat: notExist}
  147. throw err
  148. }
  149. return {srcStat, destStat}
  150. }
  151. function checkPaths (src, dest) {
  152. const {srcStat, destStat} = checkStats(src, dest)
  153. if (destStat.ino && destStat.ino === srcStat.ino) {
  154. throw new Error('Source and destination must not be the same.')
  155. }
  156. if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
  157. throw new Error(`Cannot copy '${src}' to a subdirectory of itself, '${dest}'.`)
  158. }
  159. return destStat
  160. }
  161. module.exports = copySync