blob: b0aa31ba366d6c116e9acada524d4e3227a170af [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001'use strict';
2
3var util = require('util');
4
5var fs = require('graceful-fs');
6var assign = require('object.assign');
7var date = require('value-or-function').date;
8var Writable = require('readable-stream').Writable;
9
10var constants = require('./constants');
11
12var APPEND_MODE_REGEXP = /a/;
13
14function 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
30function 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
42function getFlags(options) {
43 var flags = !options.append ? 'w' : 'a';
44 if (!options.overwrite) {
45 flags += 'x';
46 }
47 return flags;
48}
49
50function 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
64function isFatalUnlinkError(err) {
65 if (!err || err.code === 'ENOENT') {
66 return false;
67 }
68
69 return true;
70}
71
72function 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
82function 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
107function 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
141function 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
166function 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
180function 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
194function 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
282function 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 */
312function 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
346function 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)
352function 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
380util.inherits(WriteStream, Writable);
381
382WriteStream.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
400WriteStream.prototype.destroySoon = WriteStream.prototype.end;
401
402WriteStream.prototype._destroy = function(err, cb) {
403 this.close(function(err2) {
404 cb(err || err2);
405 });
406};
407
408WriteStream.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
439WriteStream.prototype._final = function(callback) {
440 if (typeof this.flush !== 'function') {
441 return callback();
442 }
443
444 this.flush(this.fd, callback);
445};
446
447function closeOnOpen() {
448 this.close();
449}
450
451WriteStream.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
480module.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};