416 lines
12 KiB
JavaScript
416 lines
12 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var Promise = global.Promise || require('promise');
|
||
|
|
||
|
var fs = require('graceful-fs');
|
||
|
var path = require('path');
|
||
|
var EventEmitter = require('events').EventEmitter;
|
||
|
var pify = require('pify');
|
||
|
var mkdirp = require('mkdirp');
|
||
|
var del = require('del');
|
||
|
var junk = require('junk');
|
||
|
var errno = require('errno');
|
||
|
var maximatch = require('maximatch');
|
||
|
var slash = require('slash');
|
||
|
var emitterMixin = require('emitter-mixin');
|
||
|
|
||
|
var CopyError = errno.custom.createError('CopyError');
|
||
|
|
||
|
var EVENT_ERROR = 'error';
|
||
|
var EVENT_COMPLETE = 'complete';
|
||
|
var EVENT_CREATE_DIRECTORY_START = 'createDirectoryStart';
|
||
|
var EVENT_CREATE_DIRECTORY_ERROR = 'createDirectoryError';
|
||
|
var EVENT_CREATE_DIRECTORY_COMPLETE = 'createDirectoryComplete';
|
||
|
var EVENT_CREATE_SYMLINK_START = 'createSymlinkStart';
|
||
|
var EVENT_CREATE_SYMLINK_ERROR = 'createSymlinkError';
|
||
|
var EVENT_CREATE_SYMLINK_COMPLETE = 'createSymlinkComplete';
|
||
|
var EVENT_COPY_FILE_START = 'copyFileStart';
|
||
|
var EVENT_COPY_FILE_ERROR = 'copyFileError';
|
||
|
var EVENT_COPY_FILE_COMPLETE = 'copyFileComplete';
|
||
|
|
||
|
var mkdir = pify(mkdirp, Promise);
|
||
|
var stat = pify(fs.stat, Promise);
|
||
|
var lstat = pify(fs.lstat, Promise);
|
||
|
var readlink = pify(fs.readlink, Promise);
|
||
|
var symlink = pify(fs.symlink, Promise);
|
||
|
var readdir = pify(fs.readdir, Promise);
|
||
|
|
||
|
module.exports = function(src, dest, options, callback) {
|
||
|
if ((arguments.length === 3) && (typeof options === 'function')) {
|
||
|
callback = options;
|
||
|
options = undefined;
|
||
|
}
|
||
|
options = options || {};
|
||
|
|
||
|
var parentDirectory = path.dirname(dest);
|
||
|
var shouldExpandSymlinks = Boolean(options.expand);
|
||
|
|
||
|
var emitter;
|
||
|
var hasFinished = false;
|
||
|
if (options.debug) { log('Ensuring output directory exists…'); }
|
||
|
var promise = ensureDirectoryExists(parentDirectory)
|
||
|
.then(function() {
|
||
|
if (options.debug) { log('Fetching source paths…'); }
|
||
|
return getFilePaths(src, shouldExpandSymlinks)
|
||
|
})
|
||
|
.then(function(filePaths) {
|
||
|
if (options.debug) { log('Filtering source paths…'); }
|
||
|
var relativePaths = filePaths.map(function(filePath) {
|
||
|
return path.relative(src, filePath);
|
||
|
});
|
||
|
var filteredPaths = getFilteredPaths(relativePaths, options.filter, {
|
||
|
dot: options.dot,
|
||
|
junk: options.junk
|
||
|
});
|
||
|
return filteredPaths.map(function(relativePath) {
|
||
|
var inputPath = relativePath;
|
||
|
var outputPath = options.rename ? options.rename(inputPath) : inputPath;
|
||
|
return {
|
||
|
src: path.join(src, inputPath),
|
||
|
dest: path.join(dest, outputPath)
|
||
|
};
|
||
|
})
|
||
|
})
|
||
|
.then(function(operations) {
|
||
|
if (options.debug) { log('Copying files…'); }
|
||
|
var hasFinishedGetter = function() { return hasFinished; };
|
||
|
var emitEvent = function() { emitter.emit.apply(emitter, arguments); };
|
||
|
return batch(operations, function(operation) {
|
||
|
return copy(operation.src, operation.dest, hasFinishedGetter, emitEvent, options);
|
||
|
}, {
|
||
|
results: options.results !== false,
|
||
|
concurrency: options.concurrency || 255
|
||
|
});
|
||
|
})
|
||
|
.catch(function(error) {
|
||
|
if (options.debug) { log('Copy failed'); }
|
||
|
if (error instanceof CopyError) {
|
||
|
emitter.emit(EVENT_ERROR, error.error, error.data);
|
||
|
throw error.error;
|
||
|
} else {
|
||
|
throw error;
|
||
|
}
|
||
|
})
|
||
|
.then(function(results) {
|
||
|
if (options.debug) { log('Copy complete'); }
|
||
|
emitter.emit(EVENT_COMPLETE, results);
|
||
|
return results;
|
||
|
})
|
||
|
.then(function(results) {
|
||
|
hasFinished = true;
|
||
|
return results;
|
||
|
})
|
||
|
.catch(function(error) {
|
||
|
hasFinished = true;
|
||
|
throw error;
|
||
|
});
|
||
|
|
||
|
if (typeof callback === 'function') {
|
||
|
promise.then(function(results) {
|
||
|
callback(null, results);
|
||
|
})
|
||
|
.catch(function(error) {
|
||
|
callback(error);
|
||
|
});
|
||
|
emitter = new EventEmitter();
|
||
|
} else {
|
||
|
emitter = emitterMixin(promise);
|
||
|
}
|
||
|
|
||
|
return emitter;
|
||
|
};
|
||
|
|
||
|
function batch(inputs, iteratee, options) {
|
||
|
var results = options.results ? [] : undefined;
|
||
|
if (inputs.length === 0) { return Promise.resolve(results); }
|
||
|
return new Promise(function(resolve, reject) {
|
||
|
var currentIndex = -1;
|
||
|
var activeWorkers = 0;
|
||
|
while (currentIndex < Math.min(inputs.length, options.concurrency) - 1) {
|
||
|
startWorker(inputs[++currentIndex]);
|
||
|
}
|
||
|
|
||
|
function startWorker(input) {
|
||
|
++activeWorkers;
|
||
|
iteratee(input).then(function(result) {
|
||
|
--activeWorkers;
|
||
|
if (results) { results.push(result); }
|
||
|
if (currentIndex < inputs.length - 1) {
|
||
|
startWorker(inputs[++currentIndex]);
|
||
|
} else if (activeWorkers === 0) {
|
||
|
resolve(results);
|
||
|
}
|
||
|
}).catch(reject);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function getFilePaths(src, shouldExpandSymlinks) {
|
||
|
return (shouldExpandSymlinks ? stat : lstat)(src)
|
||
|
.then(function(stats) {
|
||
|
if (stats.isDirectory()) {
|
||
|
return getFileListing(src, shouldExpandSymlinks)
|
||
|
.then(function(filenames) {
|
||
|
return [src].concat(filenames);
|
||
|
});
|
||
|
} else {
|
||
|
return [src];
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function getFilteredPaths(paths, filter, options) {
|
||
|
var useDotFilter = !options.dot;
|
||
|
var useJunkFilter = !options.junk;
|
||
|
if (!filter && !useDotFilter && !useJunkFilter) { return paths; }
|
||
|
return paths.filter(function(path) {
|
||
|
return (!useDotFilter || dotFilter(path)) && (!useJunkFilter || junkFilter(path)) && (!filter || (maximatch(slash(path), filter, options).length > 0));
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function dotFilter(relativePath) {
|
||
|
var filename = path.basename(relativePath);
|
||
|
return filename.charAt(0) !== '.';
|
||
|
}
|
||
|
|
||
|
function junkFilter(relativePath) {
|
||
|
var filename = path.basename(relativePath);
|
||
|
return !junk.is(filename);
|
||
|
}
|
||
|
|
||
|
function ensureDirectoryExists(path) {
|
||
|
return mkdir(path);
|
||
|
}
|
||
|
|
||
|
function getFileListing(srcPath, shouldExpandSymlinks) {
|
||
|
return readdir(srcPath)
|
||
|
.then(function(filenames) {
|
||
|
return Promise.all(
|
||
|
filenames.map(function(filename) {
|
||
|
var filePath = path.join(srcPath, filename);
|
||
|
return (shouldExpandSymlinks ? stat : lstat)(filePath)
|
||
|
.then(function(stats) {
|
||
|
if (stats.isDirectory()) {
|
||
|
return getFileListing(filePath, shouldExpandSymlinks)
|
||
|
.then(function(childPaths) {
|
||
|
return [filePath].concat(childPaths);
|
||
|
});
|
||
|
} else {
|
||
|
return [filePath];
|
||
|
}
|
||
|
});
|
||
|
})
|
||
|
)
|
||
|
.then(function mergeArrays(arrays) {
|
||
|
return Array.prototype.concat.apply([], arrays);
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function copy(srcPath, destPath, hasFinished, emitEvent, options) {
|
||
|
if (options.debug) { log('Preparing to copy ' + srcPath + '…'); }
|
||
|
return prepareForCopy(srcPath, destPath, options)
|
||
|
.then(function(stats) {
|
||
|
if (options.debug) { log('Copying ' + srcPath + '…'); }
|
||
|
var copyFunction = getCopyFunction(stats, hasFinished, emitEvent);
|
||
|
return copyFunction(srcPath, destPath, stats, options);
|
||
|
})
|
||
|
.catch(function(error) {
|
||
|
if (error instanceof CopyError) {
|
||
|
throw error;
|
||
|
}
|
||
|
var copyError = new CopyError(error.message);
|
||
|
copyError.error = error;
|
||
|
copyError.data = {
|
||
|
src: srcPath,
|
||
|
dest: destPath
|
||
|
};
|
||
|
throw copyError;
|
||
|
})
|
||
|
.then(function(result) {
|
||
|
if (options.debug) { log('Copied ' + srcPath); }
|
||
|
return result;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function prepareForCopy(srcPath, destPath, options) {
|
||
|
var shouldExpandSymlinks = Boolean(options.expand);
|
||
|
var shouldOverwriteExistingFiles = Boolean(options.overwrite);
|
||
|
return (shouldExpandSymlinks ? stat : lstat)(srcPath)
|
||
|
.then(function(stats) {
|
||
|
return ensureDestinationIsWritable(destPath, stats, shouldOverwriteExistingFiles)
|
||
|
.then(function() {
|
||
|
return stats;
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function ensureDestinationIsWritable(destPath, srcStats, shouldOverwriteExistingFiles) {
|
||
|
return lstat(destPath)
|
||
|
.catch(function(error) {
|
||
|
var shouldIgnoreError = error.code === 'ENOENT';
|
||
|
if (shouldIgnoreError) { return null; }
|
||
|
throw error;
|
||
|
})
|
||
|
.then(function(destStats) {
|
||
|
var destExists = Boolean(destStats);
|
||
|
if (!destExists) { return true; }
|
||
|
|
||
|
var isMergePossible = srcStats.isDirectory() && destStats.isDirectory();
|
||
|
if (isMergePossible) { return true; }
|
||
|
|
||
|
if (shouldOverwriteExistingFiles) {
|
||
|
return del(destPath, { force: true })
|
||
|
.then(function(paths) {
|
||
|
return true;
|
||
|
});
|
||
|
} else {
|
||
|
throw fsError('EEXIST', destPath);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function getCopyFunction(stats, hasFinished, emitEvent) {
|
||
|
if (stats.isDirectory()) {
|
||
|
return createCopyFunction(copyDirectory, stats, hasFinished, emitEvent, {
|
||
|
startEvent: EVENT_CREATE_DIRECTORY_START,
|
||
|
completeEvent: EVENT_CREATE_DIRECTORY_COMPLETE,
|
||
|
errorEvent: EVENT_CREATE_DIRECTORY_ERROR
|
||
|
});
|
||
|
} else if (stats.isSymbolicLink()) {
|
||
|
return createCopyFunction(copySymlink, stats, hasFinished, emitEvent, {
|
||
|
startEvent: EVENT_CREATE_SYMLINK_START,
|
||
|
completeEvent: EVENT_CREATE_SYMLINK_COMPLETE,
|
||
|
errorEvent: EVENT_CREATE_SYMLINK_ERROR
|
||
|
});
|
||
|
} else {
|
||
|
return createCopyFunction(copyFile, stats, hasFinished, emitEvent, {
|
||
|
startEvent: EVENT_COPY_FILE_START,
|
||
|
completeEvent: EVENT_COPY_FILE_COMPLETE,
|
||
|
errorEvent: EVENT_COPY_FILE_ERROR
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createCopyFunction(fn, stats, hasFinished, emitEvent, events) {
|
||
|
var startEvent = events.startEvent;
|
||
|
var completeEvent = events.completeEvent;
|
||
|
var errorEvent = events.errorEvent;
|
||
|
return function(srcPath, destPath, stats, options) {
|
||
|
// Multiple chains of promises are fired in parallel,
|
||
|
// so when one fails we need to prevent any future
|
||
|
// copy operations
|
||
|
if (hasFinished()) { return Promise.reject(); }
|
||
|
var metadata = {
|
||
|
src: srcPath,
|
||
|
dest: destPath,
|
||
|
stats: stats
|
||
|
};
|
||
|
emitEvent(startEvent, metadata);
|
||
|
var parentDirectory = path.dirname(destPath);
|
||
|
return ensureDirectoryExists(parentDirectory)
|
||
|
.then(function() {
|
||
|
return fn(srcPath, destPath, stats, options);
|
||
|
})
|
||
|
.then(function() {
|
||
|
if (!hasFinished()) { emitEvent(completeEvent, metadata); }
|
||
|
return metadata;
|
||
|
})
|
||
|
.catch(function(error) {
|
||
|
if (!hasFinished()) { emitEvent(errorEvent, error, metadata); }
|
||
|
throw error;
|
||
|
});
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function copyFile(srcPath, destPath, stats, options) {
|
||
|
return new Promise(function(resolve, reject) {
|
||
|
var hasFinished = false;
|
||
|
|
||
|
var read = fs.createReadStream(srcPath);
|
||
|
read.on('error', handleCopyFailed);
|
||
|
|
||
|
var write = fs.createWriteStream(destPath, {
|
||
|
flags: 'w',
|
||
|
mode: stats.mode
|
||
|
});
|
||
|
write.on('error', handleCopyFailed);
|
||
|
write.on('finish', function() {
|
||
|
fs.utimes(destPath, stats.atime, stats.mtime, function() {
|
||
|
hasFinished = true;
|
||
|
resolve();
|
||
|
});
|
||
|
});
|
||
|
|
||
|
var transformStream = null;
|
||
|
if (options.transform) {
|
||
|
transformStream = options.transform(srcPath, destPath, stats);
|
||
|
if (transformStream) {
|
||
|
transformStream.on('error', handleCopyFailed);
|
||
|
read.pipe(transformStream).pipe(write);
|
||
|
} else {
|
||
|
read.pipe(write);
|
||
|
}
|
||
|
} else {
|
||
|
read.pipe(write);
|
||
|
}
|
||
|
|
||
|
|
||
|
function handleCopyFailed(error) {
|
||
|
if (hasFinished) { return; }
|
||
|
hasFinished = true;
|
||
|
if (typeof read.close === 'function') {
|
||
|
read.close();
|
||
|
}
|
||
|
if (typeof write.close === 'function') {
|
||
|
write.close();
|
||
|
}
|
||
|
return reject(error);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function copySymlink(srcPath, destPath, stats, options) {
|
||
|
return readlink(srcPath)
|
||
|
.then(function(link) {
|
||
|
return symlink(link, destPath);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function copyDirectory(srcPath, destPath, stats, options) {
|
||
|
return mkdir(destPath)
|
||
|
.catch(function(error) {
|
||
|
var shouldIgnoreError = error.code === 'EEXIST';
|
||
|
if (shouldIgnoreError) { return; }
|
||
|
throw error;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function fsError(code, path) {
|
||
|
var errorType = errno.code[code];
|
||
|
var message = errorType.code + ', ' + errorType.description + ' ' + path;
|
||
|
var error = new Error(message);
|
||
|
error.errno = errorType.errno;
|
||
|
error.code = errorType.code;
|
||
|
error.path = path;
|
||
|
return error;
|
||
|
}
|
||
|
|
||
|
function log(message) {
|
||
|
process.stdout.write(message + '\n');
|
||
|
}
|
||
|
|
||
|
module.exports.events = {
|
||
|
ERROR: EVENT_ERROR,
|
||
|
COMPLETE: EVENT_COMPLETE,
|
||
|
CREATE_DIRECTORY_START: EVENT_CREATE_DIRECTORY_START,
|
||
|
CREATE_DIRECTORY_ERROR: EVENT_CREATE_DIRECTORY_ERROR,
|
||
|
CREATE_DIRECTORY_COMPLETE: EVENT_CREATE_DIRECTORY_COMPLETE,
|
||
|
CREATE_SYMLINK_START: EVENT_CREATE_SYMLINK_START,
|
||
|
CREATE_SYMLINK_ERROR: EVENT_CREATE_SYMLINK_ERROR,
|
||
|
CREATE_SYMLINK_COMPLETE: EVENT_CREATE_SYMLINK_COMPLETE,
|
||
|
COPY_FILE_START: EVENT_COPY_FILE_START,
|
||
|
COPY_FILE_ERROR: EVENT_COPY_FILE_ERROR,
|
||
|
COPY_FILE_COMPLETE: EVENT_COPY_FILE_COMPLETE
|
||
|
};
|