| Leo Repp | 58b9f11 | 2021-11-22 11:57:47 +0100 | [diff] [blame^] | 1 | 'use strict'; |
| 2 | |
| 3 | var util = require('util'); |
| 4 | |
| 5 | var fs = require('graceful-fs'); |
| 6 | var assign = require('object.assign'); |
| 7 | var date = require('value-or-function').date; |
| 8 | var Writable = require('readable-stream').Writable; |
| 9 | |
| 10 | var constants = require('./constants'); |
| 11 | |
| 12 | var APPEND_MODE_REGEXP = /a/; |
| 13 | |
| 14 | function closeFd(propagatedErr, fd, callback) { |
| 15 | if (typeof fd !== 'number') { |
| 16 | return callback(propagatedErr); |
| 17 | } |
| 18 | |
| 19 | fs.close(fd, onClosed); |
| 20 | |
| 21 | function onClosed(closeErr) { |
| 22 | if (propagatedErr || closeErr) { |
| 23 | return callback(propagatedErr || closeErr); |
| 24 | } |
| 25 | |
| 26 | callback(); |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | function isValidUnixId(id) { |
| 31 | if (typeof id !== 'number') { |
| 32 | return false; |
| 33 | } |
| 34 | |
| 35 | if (id < 0) { |
| 36 | return false; |
| 37 | } |
| 38 | |
| 39 | return true; |
| 40 | } |
| 41 | |
| 42 | function getFlags(options) { |
| 43 | var flags = !options.append ? 'w' : 'a'; |
| 44 | if (!options.overwrite) { |
| 45 | flags += 'x'; |
| 46 | } |
| 47 | return flags; |
| 48 | } |
| 49 | |
| 50 | function isFatalOverwriteError(err, flags) { |
| 51 | if (!err) { |
| 52 | return false; |
| 53 | } |
| 54 | |
| 55 | if (err.code === 'EEXIST' && flags[1] === 'x') { |
| 56 | // Handle scenario for file overwrite failures. |
| 57 | return false; |
| 58 | } |
| 59 | |
| 60 | // Otherwise, this is a fatal error |
| 61 | return true; |
| 62 | } |
| 63 | |
| 64 | function isFatalUnlinkError(err) { |
| 65 | if (!err || err.code === 'ENOENT') { |
| 66 | return false; |
| 67 | } |
| 68 | |
| 69 | return true; |
| 70 | } |
| 71 | |
| 72 | function getModeDiff(fsMode, vinylMode) { |
| 73 | var modeDiff = 0; |
| 74 | |
| 75 | if (typeof vinylMode === 'number') { |
| 76 | modeDiff = (vinylMode ^ fsMode) & constants.MASK_MODE; |
| 77 | } |
| 78 | |
| 79 | return modeDiff; |
| 80 | } |
| 81 | |
| 82 | function getTimesDiff(fsStat, vinylStat) { |
| 83 | |
| 84 | var mtime = date(vinylStat.mtime) || 0; |
| 85 | if (!mtime) { |
| 86 | return; |
| 87 | } |
| 88 | |
| 89 | var atime = date(vinylStat.atime) || 0; |
| 90 | if (+mtime === +fsStat.mtime && |
| 91 | +atime === +fsStat.atime) { |
| 92 | return; |
| 93 | } |
| 94 | |
| 95 | if (!atime) { |
| 96 | atime = date(fsStat.atime) || undefined; |
| 97 | } |
| 98 | |
| 99 | var timesDiff = { |
| 100 | mtime: vinylStat.mtime, |
| 101 | atime: atime, |
| 102 | }; |
| 103 | |
| 104 | return timesDiff; |
| 105 | } |
| 106 | |
| 107 | function getOwnerDiff(fsStat, vinylStat) { |
| 108 | if (!isValidUnixId(vinylStat.uid) && |
| 109 | !isValidUnixId(vinylStat.gid)) { |
| 110 | return; |
| 111 | } |
| 112 | |
| 113 | if ((!isValidUnixId(fsStat.uid) && !isValidUnixId(vinylStat.uid)) || |
| 114 | (!isValidUnixId(fsStat.gid) && !isValidUnixId(vinylStat.gid))) { |
| 115 | return; |
| 116 | } |
| 117 | |
| 118 | var uid = fsStat.uid; // Default to current uid. |
| 119 | if (isValidUnixId(vinylStat.uid)) { |
| 120 | uid = vinylStat.uid; |
| 121 | } |
| 122 | |
| 123 | var gid = fsStat.gid; // Default to current gid. |
| 124 | if (isValidUnixId(vinylStat.gid)) { |
| 125 | gid = vinylStat.gid; |
| 126 | } |
| 127 | |
| 128 | if (uid === fsStat.uid && |
| 129 | gid === fsStat.gid) { |
| 130 | return; |
| 131 | } |
| 132 | |
| 133 | var ownerDiff = { |
| 134 | uid: uid, |
| 135 | gid: gid, |
| 136 | }; |
| 137 | |
| 138 | return ownerDiff; |
| 139 | } |
| 140 | |
| 141 | function isOwner(fsStat) { |
| 142 | var hasGetuid = (typeof process.getuid === 'function'); |
| 143 | var hasGeteuid = (typeof process.geteuid === 'function'); |
| 144 | |
| 145 | // If we don't have either, assume we don't have permissions. |
| 146 | // This should only happen on Windows. |
| 147 | // Windows basically noops fchmod and errors on futimes called on directories. |
| 148 | if (!hasGeteuid && !hasGetuid) { |
| 149 | return false; |
| 150 | } |
| 151 | |
| 152 | var uid; |
| 153 | if (hasGeteuid) { |
| 154 | uid = process.geteuid(); |
| 155 | } else { |
| 156 | uid = process.getuid(); |
| 157 | } |
| 158 | |
| 159 | if (fsStat.uid !== uid && uid !== 0) { |
| 160 | return false; |
| 161 | } |
| 162 | |
| 163 | return true; |
| 164 | } |
| 165 | |
| 166 | function reflectStat(path, file, callback) { |
| 167 | // Set file.stat to the reflect current state on disk |
| 168 | fs.stat(path, onStat); |
| 169 | |
| 170 | function onStat(statErr, stat) { |
| 171 | if (statErr) { |
| 172 | return callback(statErr); |
| 173 | } |
| 174 | |
| 175 | file.stat = stat; |
| 176 | callback(); |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | function reflectLinkStat(path, file, callback) { |
| 181 | // Set file.stat to the reflect current state on disk |
| 182 | fs.lstat(path, onLstat); |
| 183 | |
| 184 | function onLstat(lstatErr, stat) { |
| 185 | if (lstatErr) { |
| 186 | return callback(lstatErr); |
| 187 | } |
| 188 | |
| 189 | file.stat = stat; |
| 190 | callback(); |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | function updateMetadata(fd, file, callback) { |
| 195 | |
| 196 | fs.fstat(fd, onStat); |
| 197 | |
| 198 | function onStat(statErr, stat) { |
| 199 | if (statErr) { |
| 200 | return callback(statErr); |
| 201 | } |
| 202 | |
| 203 | // Check if mode needs to be updated |
| 204 | var modeDiff = getModeDiff(stat.mode, file.stat.mode); |
| 205 | |
| 206 | // Check if atime/mtime need to be updated |
| 207 | var timesDiff = getTimesDiff(stat, file.stat); |
| 208 | |
| 209 | // Check if uid/gid need to be updated |
| 210 | var ownerDiff = getOwnerDiff(stat, file.stat); |
| 211 | |
| 212 | // Set file.stat to the reflect current state on disk |
| 213 | assign(file.stat, stat); |
| 214 | |
| 215 | // Nothing to do |
| 216 | if (!modeDiff && !timesDiff && !ownerDiff) { |
| 217 | return callback(); |
| 218 | } |
| 219 | |
| 220 | // Check access, `futimes`, `fchmod` & `fchown` only work if we own |
| 221 | // the file, or if we are effectively root (`fchown` only when root). |
| 222 | if (!isOwner(stat)) { |
| 223 | return callback(); |
| 224 | } |
| 225 | |
| 226 | if (modeDiff) { |
| 227 | return mode(); |
| 228 | } |
| 229 | if (timesDiff) { |
| 230 | return times(); |
| 231 | } |
| 232 | owner(); |
| 233 | |
| 234 | function mode() { |
| 235 | var mode = stat.mode ^ modeDiff; |
| 236 | |
| 237 | fs.fchmod(fd, mode, onFchmod); |
| 238 | |
| 239 | function onFchmod(fchmodErr) { |
| 240 | if (!fchmodErr) { |
| 241 | file.stat.mode = mode; |
| 242 | } |
| 243 | if (timesDiff) { |
| 244 | return times(fchmodErr); |
| 245 | } |
| 246 | if (ownerDiff) { |
| 247 | return owner(fchmodErr); |
| 248 | } |
| 249 | callback(fchmodErr); |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | function times(propagatedErr) { |
| 254 | fs.futimes(fd, timesDiff.atime, timesDiff.mtime, onFutimes); |
| 255 | |
| 256 | function onFutimes(futimesErr) { |
| 257 | if (!futimesErr) { |
| 258 | file.stat.atime = timesDiff.atime; |
| 259 | file.stat.mtime = timesDiff.mtime; |
| 260 | } |
| 261 | if (ownerDiff) { |
| 262 | return owner(propagatedErr || futimesErr); |
| 263 | } |
| 264 | callback(propagatedErr || futimesErr); |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | function owner(propagatedErr) { |
| 269 | fs.fchown(fd, ownerDiff.uid, ownerDiff.gid, onFchown); |
| 270 | |
| 271 | function onFchown(fchownErr) { |
| 272 | if (!fchownErr) { |
| 273 | file.stat.uid = ownerDiff.uid; |
| 274 | file.stat.gid = ownerDiff.gid; |
| 275 | } |
| 276 | callback(propagatedErr || fchownErr); |
| 277 | } |
| 278 | } |
| 279 | } |
| 280 | } |
| 281 | |
| 282 | function symlink(srcPath, destPath, opts, callback) { |
| 283 | // Because fs.symlink does not allow atomic overwrite option with flags, we |
| 284 | // delete and recreate if the link already exists and overwrite is true. |
| 285 | if (opts.flags === 'w') { |
| 286 | // TODO What happens when we call unlink with windows junctions? |
| 287 | fs.unlink(destPath, onUnlink); |
| 288 | } else { |
| 289 | fs.symlink(srcPath, destPath, opts.type, onSymlink); |
| 290 | } |
| 291 | |
| 292 | function onUnlink(unlinkErr) { |
| 293 | if (isFatalUnlinkError(unlinkErr)) { |
| 294 | return callback(unlinkErr); |
| 295 | } |
| 296 | fs.symlink(srcPath, destPath, opts.type, onSymlink); |
| 297 | } |
| 298 | |
| 299 | function onSymlink(symlinkErr) { |
| 300 | if (isFatalOverwriteError(symlinkErr, opts.flags)) { |
| 301 | return callback(symlinkErr); |
| 302 | } |
| 303 | callback(); |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | /* |
| 308 | Custom writeFile implementation because we need access to the |
| 309 | file descriptor after the write is complete. |
| 310 | Most of the implementation taken from node core. |
| 311 | */ |
| 312 | function writeFile(filepath, data, options, callback) { |
| 313 | if (typeof options === 'function') { |
| 314 | callback = options; |
| 315 | options = {}; |
| 316 | } |
| 317 | |
| 318 | if (!Buffer.isBuffer(data)) { |
| 319 | return callback(new TypeError('Data must be a Buffer')); |
| 320 | } |
| 321 | |
| 322 | if (!options) { |
| 323 | options = {}; |
| 324 | } |
| 325 | |
| 326 | // Default the same as node |
| 327 | var mode = options.mode || constants.DEFAULT_FILE_MODE; |
| 328 | var flags = options.flags || 'w'; |
| 329 | var position = APPEND_MODE_REGEXP.test(flags) ? null : 0; |
| 330 | |
| 331 | fs.open(filepath, flags, mode, onOpen); |
| 332 | |
| 333 | function onOpen(openErr, fd) { |
| 334 | if (openErr) { |
| 335 | return onComplete(openErr); |
| 336 | } |
| 337 | |
| 338 | fs.write(fd, data, 0, data.length, position, onComplete); |
| 339 | |
| 340 | function onComplete(writeErr) { |
| 341 | callback(writeErr, fd); |
| 342 | } |
| 343 | } |
| 344 | } |
| 345 | |
| 346 | function createWriteStream(path, options, flush) { |
| 347 | return new WriteStream(path, options, flush); |
| 348 | } |
| 349 | |
| 350 | // Taken from node core and altered to receive a flush function and simplified |
| 351 | // To be used for cleanup (like updating times/mode/etc) |
| 352 | function WriteStream(path, options, flush) { |
| 353 | // Not exposed so we can avoid the case where someone doesn't use `new` |
| 354 | |
| 355 | if (typeof options === 'function') { |
| 356 | flush = options; |
| 357 | options = null; |
| 358 | } |
| 359 | |
| 360 | options = options || {}; |
| 361 | |
| 362 | Writable.call(this, options); |
| 363 | |
| 364 | this.flush = flush; |
| 365 | this.path = path; |
| 366 | |
| 367 | this.mode = options.mode || constants.DEFAULT_FILE_MODE; |
| 368 | this.flags = options.flags || 'w'; |
| 369 | |
| 370 | // Used by node's `fs.WriteStream` |
| 371 | this.fd = null; |
| 372 | this.start = null; |
| 373 | |
| 374 | this.open(); |
| 375 | |
| 376 | // Dispose on finish. |
| 377 | this.once('finish', this.close); |
| 378 | } |
| 379 | |
| 380 | util.inherits(WriteStream, Writable); |
| 381 | |
| 382 | WriteStream.prototype.open = function() { |
| 383 | var self = this; |
| 384 | |
| 385 | fs.open(this.path, this.flags, this.mode, onOpen); |
| 386 | |
| 387 | function onOpen(openErr, fd) { |
| 388 | if (openErr) { |
| 389 | self.destroy(); |
| 390 | self.emit('error', openErr); |
| 391 | return; |
| 392 | } |
| 393 | |
| 394 | self.fd = fd; |
| 395 | self.emit('open', fd); |
| 396 | } |
| 397 | }; |
| 398 | |
| 399 | // Use our `end` method since it is patched for flush |
| 400 | WriteStream.prototype.destroySoon = WriteStream.prototype.end; |
| 401 | |
| 402 | WriteStream.prototype._destroy = function(err, cb) { |
| 403 | this.close(function(err2) { |
| 404 | cb(err || err2); |
| 405 | }); |
| 406 | }; |
| 407 | |
| 408 | WriteStream.prototype.close = function(cb) { |
| 409 | var that = this; |
| 410 | |
| 411 | if (cb) { |
| 412 | this.once('close', cb); |
| 413 | } |
| 414 | |
| 415 | if (this.closed || typeof this.fd !== 'number') { |
| 416 | if (typeof this.fd !== 'number') { |
| 417 | this.once('open', closeOnOpen); |
| 418 | return; |
| 419 | } |
| 420 | |
| 421 | return process.nextTick(function() { |
| 422 | that.emit('close'); |
| 423 | }); |
| 424 | } |
| 425 | |
| 426 | this.closed = true; |
| 427 | |
| 428 | fs.close(this.fd, function(er) { |
| 429 | if (er) { |
| 430 | that.emit('error', er); |
| 431 | } else { |
| 432 | that.emit('close'); |
| 433 | } |
| 434 | }); |
| 435 | |
| 436 | this.fd = null; |
| 437 | }; |
| 438 | |
| 439 | WriteStream.prototype._final = function(callback) { |
| 440 | if (typeof this.flush !== 'function') { |
| 441 | return callback(); |
| 442 | } |
| 443 | |
| 444 | this.flush(this.fd, callback); |
| 445 | }; |
| 446 | |
| 447 | function closeOnOpen() { |
| 448 | this.close(); |
| 449 | } |
| 450 | |
| 451 | WriteStream.prototype._write = function(data, encoding, callback) { |
| 452 | var self = this; |
| 453 | |
| 454 | // This is from node core but I have no idea how to get code coverage on it |
| 455 | if (!Buffer.isBuffer(data)) { |
| 456 | return this.emit('error', new Error('Invalid data')); |
| 457 | } |
| 458 | |
| 459 | if (typeof this.fd !== 'number') { |
| 460 | return this.once('open', onOpen); |
| 461 | } |
| 462 | |
| 463 | fs.write(this.fd, data, 0, data.length, null, onWrite); |
| 464 | |
| 465 | function onOpen() { |
| 466 | self._write(data, encoding, callback); |
| 467 | } |
| 468 | |
| 469 | function onWrite(writeErr) { |
| 470 | if (writeErr) { |
| 471 | self.destroy(); |
| 472 | callback(writeErr); |
| 473 | return; |
| 474 | } |
| 475 | |
| 476 | callback(); |
| 477 | } |
| 478 | }; |
| 479 | |
| 480 | module.exports = { |
| 481 | closeFd: closeFd, |
| 482 | isValidUnixId: isValidUnixId, |
| 483 | getFlags: getFlags, |
| 484 | isFatalOverwriteError: isFatalOverwriteError, |
| 485 | isFatalUnlinkError: isFatalUnlinkError, |
| 486 | getModeDiff: getModeDiff, |
| 487 | getTimesDiff: getTimesDiff, |
| 488 | getOwnerDiff: getOwnerDiff, |
| 489 | isOwner: isOwner, |
| 490 | reflectStat: reflectStat, |
| 491 | reflectLinkStat: reflectLinkStat, |
| 492 | updateMetadata: updateMetadata, |
| 493 | symlink: symlink, |
| 494 | writeFile: writeFile, |
| 495 | createWriteStream: createWriteStream, |
| 496 | }; |