copy-sync.js 6.9 KB

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