| '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, |
| }; |