UNPKG

fs-jetpack

Version:
267 lines (233 loc) 7.63 kB
'use strict'; var pathUtil = require('path'); var fs = require('./utils/fs'); var dir = require('./dir'); var exists = require('./exists'); var matcher = require('./utils/matcher'); var fileMode = require('./utils/mode'); var treeWalker = require('./utils/tree_walker'); var validate = require('./utils/validate'); var write = require('./write'); var validateInput = function (methodName, from, to, options) { var methodSignature = methodName + '(from, to, [options])'; validate.argument(methodSignature, 'from', from, ['string']); validate.argument(methodSignature, 'to', to, ['string']); validate.options(methodSignature, 'options', options, { overwrite: ['boolean'], matching: ['string', 'array of string'] }); }; var parseOptions = function (options, from) { var opts = options || {}; var parsedOptions = {}; parsedOptions.overwrite = opts.overwrite; if (opts.matching) { parsedOptions.allowedToCopy = matcher.create(from, opts.matching); } else { parsedOptions.allowedToCopy = function () { // Default behaviour - copy everything. return true; }; } return parsedOptions; }; var generateNoSourceError = function (path) { var err = new Error("Path to copy doesn't exist " + path); err.code = 'ENOENT'; return err; }; var generateDestinationExistsError = function (path) { var err = new Error('Destination path already exists ' + path); err.code = 'EEXIST'; return err; }; // --------------------------------------------------------- // Sync // --------------------------------------------------------- var checksBeforeCopyingSync = function (from, to, opts) { if (!exists.sync(from)) { throw generateNoSourceError(from); } if (exists.sync(to) && !opts.overwrite) { throw generateDestinationExistsError(to); } }; var copyFileSync = function (from, to, mode) { var data = fs.readFileSync(from); write.sync(to, data, { mode: mode }); }; var copySymlinkSync = function (from, to) { var symlinkPointsAt = fs.readlinkSync(from); try { fs.symlinkSync(symlinkPointsAt, to); } catch (err) { // There is already file/symlink with this name on destination location. // Must erase it manually, otherwise system won't allow us to place symlink there. if (err.code === 'EEXIST') { fs.unlinkSync(to); // Retry... fs.symlinkSync(symlinkPointsAt, to); } else { throw err; } } }; var copyItemSync = function (from, inspectData, to) { var mode = fileMode.normalizeFileMode(inspectData.mode); if (inspectData.type === 'dir') { dir.createSync(to, { mode: mode }); } else if (inspectData.type === 'file') { copyFileSync(from, to, mode); } else if (inspectData.type === 'symlink') { copySymlinkSync(from, to); } }; var copySync = function (from, to, options) { var opts = parseOptions(options, from); checksBeforeCopyingSync(from, to, opts); treeWalker.sync(from, { inspectOptions: { mode: true, symlinks: true } }, function (path, inspectData) { var rel = pathUtil.relative(from, path); var destPath = pathUtil.resolve(to, rel); if (opts.allowedToCopy(path)) { copyItemSync(path, inspectData, destPath); } }); }; // --------------------------------------------------------- // Async // --------------------------------------------------------- var checksBeforeCopyingAsync = function (from, to, opts) { return exists.async(from) .then(function (srcPathExists) { if (!srcPathExists) { throw generateNoSourceError(from); } else { return exists.async(to); } }) .then(function (destPathExists) { if (destPathExists && !opts.overwrite) { throw generateDestinationExistsError(to); } }); }; var copyFileAsync = function (from, to, mode, retriedAttempt) { return new Promise(function (resolve, reject) { var readStream = fs.createReadStream(from); var writeStream = fs.createWriteStream(to, { mode: mode }); readStream.on('error', reject); writeStream.on('error', function (err) { var toDirPath = pathUtil.dirname(to); // Force read stream to close, since write stream errored // read stream serves us no purpose. readStream.resume(); if (err.code === 'ENOENT' && retriedAttempt === undefined) { // Some parent directory doesn't exits. Create it and retry. dir.createAsync(toDirPath) .then(function () { // Make retry attempt only once to prevent vicious infinite loop // (when for some obscure reason I/O will keep returning ENOENT error). // Passing retriedAttempt = true. copyFileAsync(from, to, mode, true) .then(resolve, reject); }) .catch(reject); } else { reject(err); } }); writeStream.on('finish', resolve); readStream.pipe(writeStream); }); }; var copySymlinkAsync = function (from, to) { return fs.readlink(from) .then(function (symlinkPointsAt) { return new Promise(function (resolve, reject) { fs.symlink(symlinkPointsAt, to) .then(resolve) .catch(function (err) { if (err.code === 'EEXIST') { // There is already file/symlink with this name on destination location. // Must erase it manually, otherwise system won't allow us to place symlink there. fs.unlink(to) .then(function () { // Retry... return fs.symlink(symlinkPointsAt, to); }) .then(resolve, reject); } else { reject(err); } }); }); }); }; var copyItemAsync = function (from, inspectData, to) { var mode = fileMode.normalizeFileMode(inspectData.mode); if (inspectData.type === 'dir') { return dir.createAsync(to, { mode: mode }); } else if (inspectData.type === 'file') { return copyFileAsync(from, to, mode); } else if (inspectData.type === 'symlink') { return copySymlinkAsync(from, to); } // Ha! This is none of supported file system entities. What now? // Just continuing without actually copying sounds sane. return Promise.resolve(); }; var copyAsync = function (from, to, options) { return new Promise(function (resolve, reject) { var opts = parseOptions(options, from); checksBeforeCopyingAsync(from, to, opts) .then(function () { var allFilesDelivered = false; var filesInProgress = 0; var stream = treeWalker.stream(from, { inspectOptions: { mode: true, symlinks: true } }) .on('readable', function () { var item = stream.read(); var rel; var destPath; if (item) { rel = pathUtil.relative(from, item.path); destPath = pathUtil.resolve(to, rel); if (opts.allowedToCopy(item.path)) { filesInProgress += 1; copyItemAsync(item.path, item.item, destPath) .then(function () { filesInProgress -= 1; if (allFilesDelivered && filesInProgress === 0) { resolve(); } }) .catch(reject); } } }) .on('error', reject) .on('end', function () { allFilesDelivered = true; if (allFilesDelivered && filesInProgress === 0) { resolve(); } }); }) .catch(reject); }); }; // --------------------------------------------------------- // API // --------------------------------------------------------- exports.validateInput = validateInput; exports.sync = copySync; exports.async = copyAsync;