copy.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. 'use strict'
  2. module.exports = copy
  3. module.exports.item = copyItem
  4. module.exports.recurse = recurseDir
  5. module.exports.symlink = copySymlink
  6. module.exports.file = copyFile
  7. var nodeFs = require('fs')
  8. var path = require('path')
  9. var validate = require('aproba')
  10. var stockWriteStreamAtomic = require('fs-write-stream-atomic')
  11. var mkdirp = require('mkdirp')
  12. var rimraf = require('rimraf')
  13. var isWindows = require('./is-windows')
  14. var RunQueue = require('run-queue')
  15. var extend = Object.assign || require('util')._extend
  16. function promisify (Promise, fn) {
  17. return function () {
  18. var args = [].slice.call(arguments)
  19. return new Promise(function (resolve, reject) {
  20. return fn.apply(null, args.concat(function (err, value) {
  21. if (err) {
  22. reject(err)
  23. } else {
  24. resolve(value)
  25. }
  26. }))
  27. })
  28. }
  29. }
  30. function copy (from, to, opts) {
  31. validate('SSO|SS', arguments)
  32. opts = extend({}, opts || {})
  33. var Promise = opts.Promise || global.Promise
  34. var fs = opts.fs || nodeFs
  35. if (opts.isWindows == null) opts.isWindows = isWindows
  36. if (!opts.Promise) opts.Promise = Promise
  37. if (!opts.fs) opts.fs = fs
  38. if (!opts.recurseWith) opts.recurseWith = copyItem
  39. if (!opts.lstat) opts.lstat = promisify(opts.Promise, fs.lstat)
  40. if (!opts.stat) opts.stat = promisify(opts.Promise, fs.stat)
  41. if (!opts.chown) opts.chown = promisify(opts.Promise, fs.chown)
  42. if (!opts.readdir) opts.readdir = promisify(opts.Promise, fs.readdir)
  43. if (!opts.readlink) opts.readlink = promisify(opts.Promise, fs.readlink)
  44. if (!opts.symlink) opts.symlink = promisify(opts.Promise, fs.symlink)
  45. if (!opts.chmod) opts.chmod = promisify(opts.Promise, fs.chmod)
  46. opts.top = from
  47. opts.mkdirpAsync = promisify(opts.Promise, mkdirp)
  48. var rimrafAsync = promisify(opts.Promise, rimraf)
  49. var queue = new RunQueue({
  50. maxConcurrency: opts.maxConcurrency,
  51. Promise: Promise
  52. })
  53. opts.queue = queue
  54. queue.add(0, copyItem, [from, to, opts])
  55. return queue.run().catch(function (err) {
  56. // if the target already exists don't clobber it
  57. if (err.code === 'EEXIST' || err.code === 'EPERM') {
  58. return passThroughError()
  59. } else {
  60. return remove(to).then(passThroughError, passThroughError)
  61. }
  62. function passThroughError () {
  63. return Promise.reject(err)
  64. }
  65. })
  66. function remove (target) {
  67. var opts = {
  68. unlink: fs.unlink,
  69. chmod: fs.chmod,
  70. stat: fs.stat,
  71. lstat: fs.lstat,
  72. rmdir: fs.rmdir,
  73. readdir: fs.readdir,
  74. glob: false
  75. }
  76. return rimrafAsync(target, opts)
  77. }
  78. }
  79. function copyItem (from, to, opts) {
  80. validate('SSO', [from, to, opts])
  81. var fs = opts.fs || nodeFs
  82. var Promise = opts.Promise || global.Promise
  83. var lstat = opts.lstat || promisify(Promise, fs.lstat)
  84. return lstat(to).then(function () {
  85. return Promise.reject(eexists(from, to))
  86. }, function (err) {
  87. if (err && err.code !== 'ENOENT') return Promise.reject(err)
  88. return lstat(from)
  89. }).then(function (fromStat) {
  90. var cmdOpts = extend(extend({}, opts), fromStat)
  91. if (fromStat.isDirectory()) {
  92. return recurseDir(from, to, cmdOpts)
  93. } else if (fromStat.isSymbolicLink()) {
  94. opts.queue.add(1, copySymlink, [from, to, cmdOpts])
  95. } else if (fromStat.isFile()) {
  96. return copyFile(from, to, cmdOpts)
  97. } else if (fromStat.isBlockDevice()) {
  98. return Promise.reject(eunsupported(from + " is a block device, and we don't know how to copy those."))
  99. } else if (fromStat.isCharacterDevice()) {
  100. return Promise.reject(eunsupported(from + " is a character device, and we don't know how to copy those."))
  101. } else if (fromStat.isFIFO()) {
  102. return Promise.reject(eunsupported(from + " is a FIFO, and we don't know how to copy those."))
  103. } else if (fromStat.isSocket()) {
  104. return Promise.reject(eunsupported(from + " is a socket, and we don't know how to copy those."))
  105. } else {
  106. return Promise.reject(eunsupported("We can't tell what " + from + " is and so we can't copy it."))
  107. }
  108. })
  109. }
  110. function recurseDir (from, to, opts) {
  111. validate('SSO', [from, to, opts])
  112. var recurseWith = opts.recurseWith || copyItem
  113. var fs = opts.fs || nodeFs
  114. var chown = opts.chown || promisify(Promise, fs.chown)
  115. var readdir = opts.readdir || promisify(Promise, fs.readdir)
  116. var mkdirpAsync = opts.mkdirpAsync || promisify(Promise, mkdirp)
  117. return mkdirpAsync(to, {fs: fs, mode: opts.mode}).then(function () {
  118. var getuid = opts.getuid || process.getuid
  119. if (getuid && opts.uid != null && getuid() === 0) {
  120. return chown(to, opts.uid, opts.gid)
  121. }
  122. }).then(function () {
  123. return readdir(from)
  124. }).then(function (files) {
  125. files.forEach(function (file) {
  126. opts.queue.add(0, recurseWith, [path.join(from, file), path.join(to, file), opts])
  127. })
  128. })
  129. }
  130. function copySymlink (from, to, opts) {
  131. validate('SSO', [from, to, opts])
  132. var fs = opts.fs || nodeFs
  133. var readlink = opts.readlink || promisify(Promise, fs.readlink)
  134. var stat = opts.stat || promisify(Promise, fs.symlink)
  135. var symlink = opts.symlink || promisify(Promise, fs.symlink)
  136. var Promise = opts.Promise || global.Promise
  137. return readlink(from).then(function (fromDest) {
  138. var absoluteDest = path.resolve(path.dirname(from), fromDest)
  139. // Treat absolute paths that are inside the tree we're
  140. // copying as relative. This necessary to properly support junctions
  141. // on windows (which are always absolute) but is also DWIM with symlinks.
  142. var relativeDest = path.relative(opts.top, absoluteDest)
  143. var linkFrom = relativeDest.substr(0, 2) === '..' ? fromDest : path.relative(path.dirname(from), absoluteDest)
  144. if (opts.isWindows) {
  145. return stat(absoluteDest).catch(function () { return null }).then(function (destStat) {
  146. var isDir = destStat && destStat.isDirectory()
  147. var type = isDir ? 'dir' : 'file'
  148. return symlink(linkFrom, to, type).catch(function (err) {
  149. if (type === 'dir') {
  150. return symlink(linkFrom, to, 'junction')
  151. } else {
  152. return Promise.reject(err)
  153. }
  154. })
  155. })
  156. } else {
  157. return symlink(linkFrom, to)
  158. }
  159. })
  160. }
  161. function copyFile (from, to, opts) {
  162. validate('SSO', [from, to, opts])
  163. var fs = opts.fs || nodeFs
  164. var writeStreamAtomic = opts.writeStreamAtomic || stockWriteStreamAtomic
  165. var Promise = opts.Promise || global.Promise
  166. var chmod = opts.chmod || promisify(Promise, fs.chmod)
  167. var writeOpts = {}
  168. var getuid = opts.getuid || process.getuid
  169. if (getuid && opts.uid != null && getuid() === 0) {
  170. writeOpts.chown = {
  171. uid: opts.uid,
  172. gid: opts.gid
  173. }
  174. }
  175. return new Promise(function (resolve, reject) {
  176. var errored = false
  177. function onError (err) {
  178. errored = true
  179. reject(err)
  180. }
  181. fs.createReadStream(from)
  182. .once('error', onError)
  183. .pipe(writeStreamAtomic(to, writeOpts))
  184. .once('error', onError)
  185. .once('close', function () {
  186. if (errored) return
  187. if (opts.mode != null) {
  188. resolve(chmod(to, opts.mode))
  189. } else {
  190. resolve()
  191. }
  192. })
  193. })
  194. }
  195. function eexists (from, to) {
  196. var err = new Error('Could not move ' + from + ' to ' + to + ': destination already exists.')
  197. err.code = 'EEXIST'
  198. return err
  199. }
  200. function eunsupported (msg) {
  201. var err = new Error(msg)
  202. err.code = 'EUNSUPPORTED'
  203. return err
  204. }