460 lines
9.9 KiB
JavaScript
Raw Normal View History

'use strict';
var fs = require('graceful-fs');
var date = require('value-or-function').date;
var Writable = require('streamx').Writable;
var constants = require('./constants');
var APPEND_MODE_REGEXP = /a/;
function closeFd(propagatedErr, fd, callback) {
if (typeof fd !== 'number') {
return callback(propagatedErr);
}
fs.close(fd, onClosed);
function onClosed(closeErr) {
if (propagatedErr || closeErr) {
return callback(propagatedErr || closeErr);
}
callback();
}
}
function isValidUnixId(id) {
if (typeof id !== 'number') {
return false;
}
if (id < 0) {
return false;
}
return true;
}
function getFlags(options) {
var flags = !options.append ? 'w' : 'a';
if (!options.overwrite) {
flags += 'x';
}
return flags;
}
function isFatalOverwriteError(err, flags) {
if (!err) {
return false;
}
if (err.code === 'EEXIST' && flags[1] === 'x') {
// Handle scenario for file overwrite failures.
return false;
}
// Otherwise, this is a fatal error
return true;
}
function isFatalUnlinkError(err) {
if (!err || err.code === 'ENOENT') {
return false;
}
return true;
}
function getModeDiff(fsMode, vinylMode) {
var modeDiff = 0;
if (typeof vinylMode === 'number') {
modeDiff = (vinylMode ^ fsMode) & constants.MASK_MODE;
}
return modeDiff;
}
function getTimesDiff(fsStat, vinylStat) {
var mtime = date(vinylStat.mtime) || 0;
if (!mtime) {
return;
}
var atime = date(vinylStat.atime) || 0;
if (+mtime === +fsStat.mtime && +atime === +fsStat.atime) {
return;
}
if (!atime) {
atime = date(fsStat.atime) || undefined;
}
var timesDiff = {
mtime: vinylStat.mtime,
atime: atime,
};
return timesDiff;
}
function getOwnerDiff(fsStat, vinylStat) {
if (!isValidUnixId(vinylStat.uid) && !isValidUnixId(vinylStat.gid)) {
return;
}
if (
(!isValidUnixId(fsStat.uid) && !isValidUnixId(vinylStat.uid)) ||
(!isValidUnixId(fsStat.gid) && !isValidUnixId(vinylStat.gid))
) {
return;
}
var uid = fsStat.uid; // Default to current uid.
if (isValidUnixId(vinylStat.uid)) {
uid = vinylStat.uid;
}
var gid = fsStat.gid; // Default to current gid.
if (isValidUnixId(vinylStat.gid)) {
gid = vinylStat.gid;
}
if (uid === fsStat.uid && gid === fsStat.gid) {
return;
}
var ownerDiff = {
uid: uid,
gid: gid,
};
return ownerDiff;
}
function isOwner(fsStat) {
var hasGetuid = typeof process.getuid === 'function';
var hasGeteuid = typeof process.geteuid === 'function';
// If we don't have either, assume we don't have permissions.
// This should only happen on Windows.
// Windows basically noops fchmod and errors on futimes called on directories.
if (!hasGeteuid && !hasGetuid) {
return false;
}
var uid;
if (hasGeteuid) {
uid = process.geteuid();
} else {
uid = process.getuid();
}
if (fsStat.uid !== uid && uid !== 0) {
return false;
}
return true;
}
// Node 10 on Windows fails with EPERM if you stat a symlink to a directory so we recursively readlink before we reflect stats
// TODO: Remove this indirection when we drop Node 10 support
function findSymlinkHardpath(path, callback) {
fs.readlink(path, onReadlink);
function onReadlink(readlinkErr, resolvedPath) {
if (readlinkErr) {
return callback(readlinkErr);
}
fs.lstat(resolvedPath, onLstat);
function onLstat(lstatErr, stat) {
if (lstatErr) {
return callback(lstatErr);
}
if (stat.isSymbolicLink()) {
return findSymlinkHardpath(resolvedPath, callback);
}
callback(null, resolvedPath);
}
}
}
function reflectStat(path, file, callback) {
// Set file.stat to the reflect current state on disk
fs.stat(path, onStat);
function onStat(statErr, stat) {
if (statErr) {
return callback(statErr);
}
file.stat = stat;
callback();
}
}
function reflectLinkStat(path, file, callback) {
// Set file.stat to the reflect current state on disk
fs.lstat(path, onLstat);
function onLstat(lstatErr, stat) {
if (lstatErr) {
return callback(lstatErr);
}
file.stat = stat;
callback();
}
}
function updateMetadata(fd, file, callback) {
fs.fstat(fd, onStat);
function onStat(statErr, stat) {
if (statErr) {
return callback(statErr);
}
// Check if mode needs to be updated
var modeDiff = getModeDiff(stat.mode, file.stat.mode);
// Check if atime/mtime need to be updated
var timesDiff = getTimesDiff(stat, file.stat);
// Check if uid/gid need to be updated
var ownerDiff = getOwnerDiff(stat, file.stat);
// Set file.stat to the reflect current state on disk
Object.assign(file.stat, stat);
// Nothing to do
if (!modeDiff && !timesDiff && !ownerDiff) {
return callback();
}
// Check access, `futimes`, `fchmod` & `fchown` only work if we own
// the file, or if we are effectively root (`fchown` only when root).
if (!isOwner(stat)) {
return callback();
}
if (modeDiff) {
return mode();
}
if (timesDiff) {
return times();
}
owner();
function mode() {
var mode = stat.mode ^ modeDiff;
fs.fchmod(fd, mode, onFchmod);
function onFchmod(fchmodErr) {
if (!fchmodErr) {
file.stat.mode = mode;
}
if (timesDiff) {
return times(fchmodErr);
}
if (ownerDiff) {
return owner(fchmodErr);
}
callback(fchmodErr);
}
}
function times(propagatedErr) {
fs.futimes(fd, timesDiff.atime, timesDiff.mtime, onFutimes);
function onFutimes(futimesErr) {
if (!futimesErr) {
file.stat.atime = timesDiff.atime;
file.stat.mtime = timesDiff.mtime;
}
// If a filesystem doesn't implement futimes, we don't callback with the error.
// Instead we update the stats to match filesystem and clear the error.
if (futimesErr && futimesErr.code === 'ENOSYS') {
file.stat.atime = stat.atime;
file.stat.mtime = stat.mtime;
futimesErr = null;
}
if (ownerDiff) {
return owner(propagatedErr || futimesErr);
}
callback(propagatedErr || futimesErr);
}
}
function owner(propagatedErr) {
fs.fchown(fd, ownerDiff.uid, ownerDiff.gid, onFchown);
function onFchown(fchownErr) {
if (!fchownErr) {
file.stat.uid = ownerDiff.uid;
file.stat.gid = ownerDiff.gid;
}
callback(propagatedErr || fchownErr);
}
}
}
}
function symlink(srcPath, destPath, opts, callback) {
// Because fs.symlink does not allow atomic overwrite option with flags, we
// delete and recreate if the link already exists and overwrite is true.
if (opts.flags === 'w') {
// TODO What happens when we call unlink with windows junctions?
fs.unlink(destPath, onUnlink);
} else {
fs.symlink(srcPath, destPath, opts.type, onSymlink);
}
function onUnlink(unlinkErr) {
if (isFatalUnlinkError(unlinkErr)) {
return callback(unlinkErr);
}
fs.symlink(srcPath, destPath, opts.type, onSymlink);
}
function onSymlink(symlinkErr) {
if (isFatalOverwriteError(symlinkErr, opts.flags)) {
return callback(symlinkErr);
}
callback();
}
}
/*
Custom writeFile implementation because we need access to the
file descriptor after the write is complete.
Most of the implementation taken from node core.
*/
function writeFile(filepath, data, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (!Buffer.isBuffer(data)) {
return callback(new TypeError('Data must be a Buffer'));
}
if (!options) {
options = {};
}
// Default the same as node
var mode = options.mode || constants.DEFAULT_FILE_MODE;
var flags = options.flags || 'w';
var position = APPEND_MODE_REGEXP.test(flags) ? null : 0;
fs.open(filepath, flags, mode, onOpen);
function onOpen(openErr, fd) {
if (openErr) {
return onComplete(openErr);
}
fs.write(fd, data, 0, data.length, position, onComplete);
function onComplete(writeErr) {
callback(writeErr, fd);
}
}
}
function noopFlush(fd, cb) {
cb();
}
function createWriteStream(path, options, flush) {
if (typeof options === 'function') {
flush = options;
options = null;
}
options = options || {};
flush = flush || noopFlush;
var mode = options.mode || constants.DEFAULT_FILE_MODE;
var flags = options.flags || 'w';
var fd = null;
return new Writable({
mapWritable: function (data) {
if (typeof data === 'string') {
return Buffer.from(data);
} else {
return data;
}
},
open: function (cb) {
fs.open(path, flags, mode, onOpen);
function onOpen(openErr, openedFd) {
if (openErr) {
cb(openErr);
return;
}
fd = openedFd;
cb();
}
},
destroy: function (cb) {
if (fd) {
fs.close(fd, onClose);
} else {
onClose();
}
function onClose(closeErr) {
fd = null;
cb(closeErr);
}
},
write: function (data, cb) {
fs.write(fd, data, 0, data.length, null, onWrite);
function onWrite(writeErr) {
if (writeErr) {
cb(writeErr);
return;
}
cb();
}
},
final: function (cb) {
flush(fd, cb);
},
});
}
module.exports = {
closeFd: closeFd,
isValidUnixId: isValidUnixId,
getFlags: getFlags,
isFatalOverwriteError: isFatalOverwriteError,
isFatalUnlinkError: isFatalUnlinkError,
getModeDiff: getModeDiff,
getTimesDiff: getTimesDiff,
getOwnerDiff: getOwnerDiff,
isOwner: isOwner,
findSymlinkHardpath: findSymlinkHardpath,
reflectStat: reflectStat,
reflectLinkStat: reflectLinkStat,
updateMetadata: updateMetadata,
symlink: symlink,
writeFile: writeFile,
createWriteStream: createWriteStream,
};