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} 226