fs-jetpack
Version:
Better file system API
267 lines (233 loc) • 7.63 kB
JavaScript
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;
;