tmp.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. /*!
  2. * Tmp
  3. *
  4. * Copyright (c) 2011-2017 KARASZI Istvan <github@spam.raszi.hu>
  5. *
  6. * MIT Licensed
  7. */
  8. /*
  9. * Module dependencies.
  10. */
  11. const fs = require('fs');
  12. const path = require('path');
  13. const crypto = require('crypto');
  14. const osTmpDir = require('os-tmpdir');
  15. const _c = process.binding('constants');
  16. /*
  17. * The working inner variables.
  18. */
  19. const
  20. /**
  21. * The temporary directory.
  22. * @type {string}
  23. */
  24. tmpDir = osTmpDir(),
  25. // the random characters to choose from
  26. RANDOM_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
  27. TEMPLATE_PATTERN = /XXXXXX/,
  28. DEFAULT_TRIES = 3,
  29. CREATE_FLAGS = (_c.O_CREAT || _c.fs.O_CREAT) | (_c.O_EXCL || _c.fs.O_EXCL) | (_c.O_RDWR || _c.fs.O_RDWR),
  30. EBADF = _c.EBADF || _c.os.errno.EBADF,
  31. ENOENT = _c.ENOENT || _c.os.errno.ENOENT,
  32. DIR_MODE = 448 /* 0o700 */,
  33. FILE_MODE = 384 /* 0o600 */,
  34. // this will hold the objects need to be removed on exit
  35. _removeObjects = [];
  36. var
  37. _gracefulCleanup = false,
  38. _uncaughtException = false;
  39. /**
  40. * Random name generator based on crypto.
  41. * Adapted from http://blog.tompawlak.org/how-to-generate-random-values-nodejs-javascript
  42. *
  43. * @param {number} howMany
  44. * @returns {string} the generated random name
  45. * @private
  46. */
  47. function _randomChars(howMany) {
  48. var
  49. value = [],
  50. rnd = null;
  51. // make sure that we do not fail because we ran out of entropy
  52. try {
  53. rnd = crypto.randomBytes(howMany);
  54. } catch (e) {
  55. rnd = crypto.pseudoRandomBytes(howMany);
  56. }
  57. for (var i = 0; i < howMany; i++) {
  58. value.push(RANDOM_CHARS[rnd[i] % RANDOM_CHARS.length]);
  59. }
  60. return value.join('');
  61. }
  62. /**
  63. * Checks whether the `obj` parameter is defined or not.
  64. *
  65. * @param {Object} obj
  66. * @returns {boolean} true if the object is undefined
  67. * @private
  68. */
  69. function _isUndefined(obj) {
  70. return typeof obj === 'undefined';
  71. }
  72. /**
  73. * Parses the function arguments.
  74. *
  75. * This function helps to have optional arguments.
  76. *
  77. * @param {(Options|Function)} options
  78. * @param {Function} callback
  79. * @returns {Array} parsed arguments
  80. * @private
  81. */
  82. function _parseArguments(options, callback) {
  83. if (typeof options == 'function') {
  84. return [callback || {}, options];
  85. }
  86. if (_isUndefined(options)) {
  87. return [{}, callback];
  88. }
  89. return [options, callback];
  90. }
  91. /**
  92. * Generates a new temporary name.
  93. *
  94. * @param {Object} opts
  95. * @returns {string} the new random name according to opts
  96. * @private
  97. */
  98. function _generateTmpName(opts) {
  99. if (opts.name) {
  100. return path.join(opts.dir || tmpDir, opts.name);
  101. }
  102. // mkstemps like template
  103. if (opts.template) {
  104. return opts.template.replace(TEMPLATE_PATTERN, _randomChars(6));
  105. }
  106. // prefix and postfix
  107. const name = [
  108. opts.prefix || 'tmp-',
  109. process.pid,
  110. _randomChars(12),
  111. opts.postfix || ''
  112. ].join('');
  113. return path.join(opts.dir || tmpDir, name);
  114. }
  115. /**
  116. * Gets a temporary file name.
  117. *
  118. * @param {(Options|tmpNameCallback)} options options or callback
  119. * @param {?tmpNameCallback} callback the callback function
  120. */
  121. function tmpName(options, callback) {
  122. var
  123. args = _parseArguments(options, callback),
  124. opts = args[0],
  125. cb = args[1],
  126. tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
  127. if (isNaN(tries) || tries < 0)
  128. return cb(new Error('Invalid tries'));
  129. if (opts.template && !opts.template.match(TEMPLATE_PATTERN))
  130. return cb(new Error('Invalid template provided'));
  131. (function _getUniqueName() {
  132. const name = _generateTmpName(opts);
  133. // check whether the path exists then retry if needed
  134. fs.stat(name, function (err) {
  135. if (!err) {
  136. if (tries-- > 0) return _getUniqueName();
  137. return cb(new Error('Could not get a unique tmp filename, max tries reached ' + name));
  138. }
  139. cb(null, name);
  140. });
  141. }());
  142. }
  143. /**
  144. * Synchronous version of tmpName.
  145. *
  146. * @param {Object} options
  147. * @returns {string} the generated random name
  148. * @throws {Error} if the options are invalid or could not generate a filename
  149. */
  150. function tmpNameSync(options) {
  151. var
  152. args = _parseArguments(options),
  153. opts = args[0],
  154. tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
  155. if (isNaN(tries) || tries < 0)
  156. throw new Error('Invalid tries');
  157. if (opts.template && !opts.template.match(TEMPLATE_PATTERN))
  158. throw new Error('Invalid template provided');
  159. do {
  160. const name = _generateTmpName(opts);
  161. try {
  162. fs.statSync(name);
  163. } catch (e) {
  164. return name;
  165. }
  166. } while (tries-- > 0);
  167. throw new Error('Could not get a unique tmp filename, max tries reached');
  168. }
  169. /**
  170. * Creates and opens a temporary file.
  171. *
  172. * @param {(Options|fileCallback)} options the config options or the callback function
  173. * @param {?fileCallback} callback
  174. */
  175. function file(options, callback) {
  176. var
  177. args = _parseArguments(options, callback),
  178. opts = args[0],
  179. cb = args[1];
  180. opts.postfix = (_isUndefined(opts.postfix)) ? '.tmp' : opts.postfix;
  181. // gets a temporary filename
  182. tmpName(opts, function _tmpNameCreated(err, name) {
  183. if (err) return cb(err);
  184. // create and open the file
  185. fs.open(name, CREATE_FLAGS, opts.mode || FILE_MODE, function _fileCreated(err, fd) {
  186. if (err) return cb(err);
  187. if (opts.discardDescriptor) {
  188. return fs.close(fd, function _discardCallback(err) {
  189. if (err) {
  190. // Low probability, and the file exists, so this could be
  191. // ignored. If it isn't we certainly need to unlink the
  192. // file, and if that fails too its error is more
  193. // important.
  194. try {
  195. fs.unlinkSync(name);
  196. } catch (e) {
  197. if (!isENOENT(e)) {
  198. err = e;
  199. }
  200. }
  201. return cb(err);
  202. }
  203. cb(null, name, undefined, _prepareTmpFileRemoveCallback(name, -1, opts));
  204. });
  205. }
  206. if (opts.detachDescriptor) {
  207. return cb(null, name, fd, _prepareTmpFileRemoveCallback(name, -1, opts));
  208. }
  209. cb(null, name, fd, _prepareTmpFileRemoveCallback(name, fd, opts));
  210. });
  211. });
  212. }
  213. /**
  214. * Synchronous version of file.
  215. *
  216. * @param {Options} options
  217. * @returns {FileSyncObject} object consists of name, fd and removeCallback
  218. * @throws {Error} if cannot create a file
  219. */
  220. function fileSync(options) {
  221. var
  222. args = _parseArguments(options),
  223. opts = args[0];
  224. opts.postfix = opts.postfix || '.tmp';
  225. const discardOrDetachDescriptor = opts.discardDescriptor || opts.detachDescriptor;
  226. const name = tmpNameSync(opts);
  227. var fd = fs.openSync(name, CREATE_FLAGS, opts.mode || FILE_MODE);
  228. if (opts.discardDescriptor) {
  229. fs.closeSync(fd);
  230. fd = undefined;
  231. }
  232. return {
  233. name: name,
  234. fd: fd,
  235. removeCallback: _prepareTmpFileRemoveCallback(name, discardOrDetachDescriptor ? -1 : fd, opts)
  236. };
  237. }
  238. /**
  239. * Removes files and folders in a directory recursively.
  240. *
  241. * @param {string} root
  242. * @private
  243. */
  244. function _rmdirRecursiveSync(root) {
  245. const dirs = [root];
  246. do {
  247. var
  248. dir = dirs.pop(),
  249. deferred = false,
  250. files = fs.readdirSync(dir);
  251. for (var i = 0, length = files.length; i < length; i++) {
  252. var
  253. file = path.join(dir, files[i]),
  254. stat = fs.lstatSync(file); // lstat so we don't recurse into symlinked directories
  255. if (stat.isDirectory()) {
  256. if (!deferred) {
  257. deferred = true;
  258. dirs.push(dir);
  259. }
  260. dirs.push(file);
  261. } else {
  262. fs.unlinkSync(file);
  263. }
  264. }
  265. if (!deferred) {
  266. fs.rmdirSync(dir);
  267. }
  268. } while (dirs.length !== 0);
  269. }
  270. /**
  271. * Creates a temporary directory.
  272. *
  273. * @param {(Options|dirCallback)} options the options or the callback function
  274. * @param {?dirCallback} callback
  275. */
  276. function dir(options, callback) {
  277. var
  278. args = _parseArguments(options, callback),
  279. opts = args[0],
  280. cb = args[1];
  281. // gets a temporary filename
  282. tmpName(opts, function _tmpNameCreated(err, name) {
  283. if (err) return cb(err);
  284. // create the directory
  285. fs.mkdir(name, opts.mode || DIR_MODE, function _dirCreated(err) {
  286. if (err) return cb(err);
  287. cb(null, name, _prepareTmpDirRemoveCallback(name, opts));
  288. });
  289. });
  290. }
  291. /**
  292. * Synchronous version of dir.
  293. *
  294. * @param {Options} options
  295. * @returns {DirSyncObject} object consists of name and removeCallback
  296. * @throws {Error} if it cannot create a directory
  297. */
  298. function dirSync(options) {
  299. var
  300. args = _parseArguments(options),
  301. opts = args[0];
  302. const name = tmpNameSync(opts);
  303. fs.mkdirSync(name, opts.mode || DIR_MODE);
  304. return {
  305. name: name,
  306. removeCallback: _prepareTmpDirRemoveCallback(name, opts)
  307. };
  308. }
  309. /**
  310. * Prepares the callback for removal of the temporary file.
  311. *
  312. * @param {string} name the path of the file
  313. * @param {number} fd file descriptor
  314. * @param {Object} opts
  315. * @returns {fileCallback}
  316. * @private
  317. */
  318. function _prepareTmpFileRemoveCallback(name, fd, opts) {
  319. const removeCallback = _prepareRemoveCallback(function _removeCallback(fdPath) {
  320. try {
  321. if (0 <= fdPath[0]) {
  322. fs.closeSync(fdPath[0]);
  323. }
  324. }
  325. catch (e) {
  326. // under some node/windows related circumstances, a temporary file
  327. // may have not be created as expected or the file was already closed
  328. // by the user, in which case we will simply ignore the error
  329. if (!isEBADF(e) && !isENOENT(e)) {
  330. // reraise any unanticipated error
  331. throw e;
  332. }
  333. }
  334. try {
  335. fs.unlinkSync(fdPath[1]);
  336. }
  337. catch (e) {
  338. if (!isENOENT(e)) {
  339. // reraise any unanticipated error
  340. throw e;
  341. }
  342. }
  343. }, [fd, name]);
  344. if (!opts.keep) {
  345. _removeObjects.unshift(removeCallback);
  346. }
  347. return removeCallback;
  348. }
  349. /**
  350. * Prepares the callback for removal of the temporary directory.
  351. *
  352. * @param {string} name
  353. * @param {Object} opts
  354. * @returns {Function} the callback
  355. * @private
  356. */
  357. function _prepareTmpDirRemoveCallback(name, opts) {
  358. const removeFunction = opts.unsafeCleanup ? _rmdirRecursiveSync : fs.rmdirSync.bind(fs);
  359. const removeCallback = _prepareRemoveCallback(removeFunction, name);
  360. if (!opts.keep) {
  361. _removeObjects.unshift(removeCallback);
  362. }
  363. return removeCallback;
  364. }
  365. /**
  366. * Creates a guarded function wrapping the removeFunction call.
  367. *
  368. * @param {Function} removeFunction
  369. * @param {Object} arg
  370. * @returns {Function}
  371. * @private
  372. */
  373. function _prepareRemoveCallback(removeFunction, arg) {
  374. var called = false;
  375. return function _cleanupCallback(next) {
  376. if (!called) {
  377. const index = _removeObjects.indexOf(_cleanupCallback);
  378. if (index >= 0) {
  379. _removeObjects.splice(index, 1);
  380. }
  381. called = true;
  382. removeFunction(arg);
  383. }
  384. if (next) next(null);
  385. };
  386. }
  387. /**
  388. * The garbage collector.
  389. *
  390. * @private
  391. */
  392. function _garbageCollector() {
  393. if (_uncaughtException && !_gracefulCleanup) {
  394. return;
  395. }
  396. // the function being called removes itself from _removeObjects,
  397. // loop until _removeObjects is empty
  398. while (_removeObjects.length) {
  399. try {
  400. _removeObjects[0].call(null);
  401. } catch (e) {
  402. // already removed?
  403. }
  404. }
  405. }
  406. /**
  407. * Helper for testing against EBADF to compensate changes made to Node 7.x under Windows.
  408. */
  409. function isEBADF(error) {
  410. return isExpectedError(error, -EBADF, 'EBADF');
  411. }
  412. /**
  413. * Helper for testing against ENOENT to compensate changes made to Node 7.x under Windows.
  414. */
  415. function isENOENT(error) {
  416. return isExpectedError(error, -ENOENT, 'ENOENT');
  417. }
  418. /**
  419. * Helper to determine whether the expected error code matches the actual code and errno,
  420. * which will differ between the supported node versions.
  421. *
  422. * - Node >= 7.0:
  423. * error.code {String}
  424. * error.errno {String|Number} any numerical value will be negated
  425. *
  426. * - Node >= 6.0 < 7.0:
  427. * error.code {String}
  428. * error.errno {Number} negated
  429. *
  430. * - Node >= 4.0 < 6.0: introduces SystemError
  431. * error.code {String}
  432. * error.errno {Number} negated
  433. *
  434. * - Node >= 0.10 < 4.0:
  435. * error.code {Number} negated
  436. * error.errno n/a
  437. */
  438. function isExpectedError(error, code, errno) {
  439. return error.code == code || error.code == errno;
  440. }
  441. /**
  442. * Sets the graceful cleanup.
  443. *
  444. * Also removes the created files and directories when an uncaught exception occurs.
  445. */
  446. function setGracefulCleanup() {
  447. _gracefulCleanup = true;
  448. }
  449. const version = process.versions.node.split('.').map(function (value) {
  450. return parseInt(value, 10);
  451. });
  452. if (version[0] === 0 && (version[1] < 9 || version[1] === 9 && version[2] < 5)) {
  453. process.addListener('uncaughtException', function _uncaughtExceptionThrown(err) {
  454. _uncaughtException = true;
  455. _garbageCollector();
  456. throw err;
  457. });
  458. }
  459. process.addListener('exit', function _exit(code) {
  460. if (code) _uncaughtException = true;
  461. _garbageCollector();
  462. });
  463. /**
  464. * Configuration options.
  465. *
  466. * @typedef {Object} Options
  467. * @property {?number} tries the number of tries before give up the name generation
  468. * @property {?string} template the "mkstemp" like filename template
  469. * @property {?string} name fix name
  470. * @property {?string} dir the tmp directory to use
  471. * @property {?string} prefix prefix for the generated name
  472. * @property {?string} postfix postfix for the generated name
  473. */
  474. /**
  475. * @typedef {Object} FileSyncObject
  476. * @property {string} name the name of the file
  477. * @property {string} fd the file descriptor
  478. * @property {fileCallback} removeCallback the callback function to remove the file
  479. */
  480. /**
  481. * @typedef {Object} DirSyncObject
  482. * @property {string} name the name of the directory
  483. * @property {fileCallback} removeCallback the callback function to remove the directory
  484. */
  485. /**
  486. * @callback tmpNameCallback
  487. * @param {?Error} err the error object if anything goes wrong
  488. * @param {string} name the temporary file name
  489. */
  490. /**
  491. * @callback fileCallback
  492. * @param {?Error} err the error object if anything goes wrong
  493. * @param {string} name the temporary file name
  494. * @param {number} fd the file descriptor
  495. * @param {cleanupCallback} fn the cleanup callback function
  496. */
  497. /**
  498. * @callback dirCallback
  499. * @param {?Error} err the error object if anything goes wrong
  500. * @param {string} name the temporary file name
  501. * @param {cleanupCallback} fn the cleanup callback function
  502. */
  503. /**
  504. * Removes the temporary created file or directory.
  505. *
  506. * @callback cleanupCallback
  507. * @param {simpleCallback} [next] function to call after entry was removed
  508. */
  509. /**
  510. * Callback function for function composition.
  511. * @see {@link https://github.com/raszi/node-tmp/issues/57|raszi/node-tmp#57}
  512. *
  513. * @callback simpleCallback
  514. */
  515. // exporting all the needed methods
  516. module.exports.tmpdir = tmpDir;
  517. module.exports.dir = dir;
  518. module.exports.dirSync = dirSync;
  519. module.exports.file = file;
  520. module.exports.fileSync = fileSync;
  521. module.exports.tmpName = tmpName;
  522. module.exports.tmpNameSync = tmpNameSync;
  523. module.exports.setGracefulCleanup = setGracefulCleanup;