496 lines
10 KiB
JavaScript
496 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
var util = require('util');
|
|
|
|
var fs = require('graceful-fs');
|
|
var assign = require('object.assign');
|
|
var date = require('value-or-function').date;
|
|
var Writable = require('readable-stream').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;
|
|
}
|
|
|
|
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
|
|
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 (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 createWriteStream(path, options, flush) {
|
|
return new WriteStream(path, options, flush);
|
|
}
|
|
|
|
// Taken from node core and altered to receive a flush function and simplified
|
|
// To be used for cleanup (like updating times/mode/etc)
|
|
function WriteStream(path, options, flush) {
|
|
// Not exposed so we can avoid the case where someone doesn't use `new`
|
|
|
|
if (typeof options === 'function') {
|
|
flush = options;
|
|
options = null;
|
|
}
|
|
|
|
options = options || {};
|
|
|
|
Writable.call(this, options);
|
|
|
|
this.flush = flush;
|
|
this.path = path;
|
|
|
|
this.mode = options.mode || constants.DEFAULT_FILE_MODE;
|
|
this.flags = options.flags || 'w';
|
|
|
|
// Used by node's `fs.WriteStream`
|
|
this.fd = null;
|
|
this.start = null;
|
|
|
|
this.open();
|
|
|
|
// Dispose on finish.
|
|
this.once('finish', this.close);
|
|
}
|
|
|
|
util.inherits(WriteStream, Writable);
|
|
|
|
WriteStream.prototype.open = function() {
|
|
var self = this;
|
|
|
|
fs.open(this.path, this.flags, this.mode, onOpen);
|
|
|
|
function onOpen(openErr, fd) {
|
|
if (openErr) {
|
|
self.destroy();
|
|
self.emit('error', openErr);
|
|
return;
|
|
}
|
|
|
|
self.fd = fd;
|
|
self.emit('open', fd);
|
|
}
|
|
};
|
|
|
|
// Use our `end` method since it is patched for flush
|
|
WriteStream.prototype.destroySoon = WriteStream.prototype.end;
|
|
|
|
WriteStream.prototype._destroy = function(err, cb) {
|
|
this.close(function(err2) {
|
|
cb(err || err2);
|
|
});
|
|
};
|
|
|
|
WriteStream.prototype.close = function(cb) {
|
|
var that = this;
|
|
|
|
if (cb) {
|
|
this.once('close', cb);
|
|
}
|
|
|
|
if (this.closed || typeof this.fd !== 'number') {
|
|
if (typeof this.fd !== 'number') {
|
|
this.once('open', closeOnOpen);
|
|
return;
|
|
}
|
|
|
|
return process.nextTick(function() {
|
|
that.emit('close');
|
|
});
|
|
}
|
|
|
|
this.closed = true;
|
|
|
|
fs.close(this.fd, function(er) {
|
|
if (er) {
|
|
that.emit('error', er);
|
|
} else {
|
|
that.emit('close');
|
|
}
|
|
});
|
|
|
|
this.fd = null;
|
|
};
|
|
|
|
WriteStream.prototype._final = function(callback) {
|
|
if (typeof this.flush !== 'function') {
|
|
return callback();
|
|
}
|
|
|
|
this.flush(this.fd, callback);
|
|
};
|
|
|
|
function closeOnOpen() {
|
|
this.close();
|
|
}
|
|
|
|
WriteStream.prototype._write = function(data, encoding, callback) {
|
|
var self = this;
|
|
|
|
// This is from node core but I have no idea how to get code coverage on it
|
|
if (!Buffer.isBuffer(data)) {
|
|
return this.emit('error', new Error('Invalid data'));
|
|
}
|
|
|
|
if (typeof this.fd !== 'number') {
|
|
return this.once('open', onOpen);
|
|
}
|
|
|
|
fs.write(this.fd, data, 0, data.length, null, onWrite);
|
|
|
|
function onOpen() {
|
|
self._write(data, encoding, callback);
|
|
}
|
|
|
|
function onWrite(writeErr) {
|
|
if (writeErr) {
|
|
self.destroy();
|
|
callback(writeErr);
|
|
return;
|
|
}
|
|
|
|
callback();
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
closeFd: closeFd,
|
|
isValidUnixId: isValidUnixId,
|
|
getFlags: getFlags,
|
|
isFatalOverwriteError: isFatalOverwriteError,
|
|
isFatalUnlinkError: isFatalUnlinkError,
|
|
getModeDiff: getModeDiff,
|
|
getTimesDiff: getTimesDiff,
|
|
getOwnerDiff: getOwnerDiff,
|
|
isOwner: isOwner,
|
|
reflectStat: reflectStat,
|
|
reflectLinkStat: reflectLinkStat,
|
|
updateMetadata: updateMetadata,
|
|
symlink: symlink,
|
|
writeFile: writeFile,
|
|
createWriteStream: createWriteStream,
|
|
};
|