UNPKG

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