You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

file-operations.js 10KB

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