4 * Copyright (c) 2011-2017 KARASZI Istvan <github@spam.raszi.hu>
10 * Module dependencies.
12 const fs = require('fs');
13 const path = require('path');
14 const crypto = require('crypto');
15 const osTmpDir = require('os-tmpdir');
16 const _c = process.binding('constants');
19 * The working inner variables.
23 * The temporary directory.
28 // the random characters to choose from
29 RANDOM_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
31 TEMPLATE_PATTERN = /XXXXXX/,
35 CREATE_FLAGS = (_c.O_CREAT || _c.fs.O_CREAT) | (_c.O_EXCL || _c.fs.O_EXCL) | (_c.O_RDWR || _c.fs.O_RDWR),
37 EBADF = _c.EBADF || _c.os.errno.EBADF,
38 ENOENT = _c.ENOENT || _c.os.errno.ENOENT,
40 DIR_MODE = 448 /* 0o700 */,
41 FILE_MODE = 384 /* 0o600 */,
43 // this will hold the objects need to be removed on exit
47 _gracefulCleanup = false,
48 _uncaughtException = false;
51 * Random name generator based on crypto.
52 * Adapted from http://blog.tompawlak.org/how-to-generate-random-values-nodejs-javascript
54 * @param {number} howMany
55 * @returns {string} the generated random name
58 function _randomChars(howMany) {
63 // make sure that we do not fail because we ran out of entropy
65 rnd = crypto.randomBytes(howMany);
67 rnd = crypto.pseudoRandomBytes(howMany);
70 for (var i = 0; i < howMany; i++) {
71 value.push(RANDOM_CHARS[rnd[i] % RANDOM_CHARS.length]);
74 return value.join('');
78 * Checks whether the `obj` parameter is defined or not.
81 * @returns {boolean} true if the object is undefined
84 function _isUndefined(obj) {
85 return typeof obj === 'undefined';
89 * Parses the function arguments.
91 * This function helps to have optional arguments.
93 * @param {(Options|Function)} options
94 * @param {Function} callback
95 * @returns {Array} parsed arguments
98 function _parseArguments(options, callback) {
99 if (typeof options == 'function') {
100 return [callback || {}, options];
103 if (_isUndefined(options)) {
104 return [{}, callback];
107 return [options, callback];
111 * Generates a new temporary name.
113 * @param {Object} opts
114 * @returns {string} the new random name according to opts
117 function _generateTmpName(opts) {
119 return path.join(opts.dir || tmpDir, opts.name);
122 // mkstemps like template
124 return opts.template.replace(TEMPLATE_PATTERN, _randomChars(6));
127 // prefix and postfix
129 opts.prefix || 'tmp-',
135 return path.join(opts.dir || tmpDir, name);
139 * Gets a temporary file name.
141 * @param {(Options|tmpNameCallback)} options options or callback
142 * @param {?tmpNameCallback} callback the callback function
144 function tmpName(options, callback) {
146 args = _parseArguments(options, callback),
149 tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
151 if (isNaN(tries) || tries < 0)
152 return cb(new Error('Invalid tries'));
154 if (opts.template && !opts.template.match(TEMPLATE_PATTERN))
155 return cb(new Error('Invalid template provided'));
157 (function _getUniqueName() {
158 const name = _generateTmpName(opts);
160 // check whether the path exists then retry if needed
161 fs.stat(name, function (err) {
163 if (tries-- > 0) return _getUniqueName();
165 return cb(new Error('Could not get a unique tmp filename, max tries reached ' + name));
174 * Synchronous version of tmpName.
176 * @param {Object} options
177 * @returns {string} the generated random name
178 * @throws {Error} if the options are invalid or could not generate a filename
180 function tmpNameSync(options) {
182 args = _parseArguments(options),
184 tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
186 if (isNaN(tries) || tries < 0)
187 throw new Error('Invalid tries');
189 if (opts.template && !opts.template.match(TEMPLATE_PATTERN))
190 throw new Error('Invalid template provided');
193 const name = _generateTmpName(opts);
199 } while (tries-- > 0);
201 throw new Error('Could not get a unique tmp filename, max tries reached');
205 * Creates and opens a temporary file.
207 * @param {(Options|fileCallback)} options the config options or the callback function
208 * @param {?fileCallback} callback
210 function file(options, callback) {
212 args = _parseArguments(options, callback),
216 opts.postfix = (_isUndefined(opts.postfix)) ? '.tmp' : opts.postfix;
218 // gets a temporary filename
219 tmpName(opts, function _tmpNameCreated(err, name) {
220 if (err) return cb(err);
222 // create and open the file
223 fs.open(name, CREATE_FLAGS, opts.mode || FILE_MODE, function _fileCreated(err, fd) {
224 if (err) return cb(err);
226 if (opts.discardDescriptor) {
227 return fs.close(fd, function _discardCallback(err) {
229 // Low probability, and the file exists, so this could be
230 // ignored. If it isn't we certainly need to unlink the
231 // file, and if that fails too its error is more
242 cb(null, name, undefined, _prepareTmpFileRemoveCallback(name, -1, opts));
245 if (opts.detachDescriptor) {
246 return cb(null, name, fd, _prepareTmpFileRemoveCallback(name, -1, opts));
248 cb(null, name, fd, _prepareTmpFileRemoveCallback(name, fd, opts));
254 * Synchronous version of file.
256 * @param {Options} options
257 * @returns {FileSyncObject} object consists of name, fd and removeCallback
258 * @throws {Error} if cannot create a file
260 function fileSync(options) {
262 args = _parseArguments(options),
265 opts.postfix = opts.postfix || '.tmp';
267 const discardOrDetachDescriptor = opts.discardDescriptor || opts.detachDescriptor;
268 const name = tmpNameSync(opts);
269 var fd = fs.openSync(name, CREATE_FLAGS, opts.mode || FILE_MODE);
270 if (opts.discardDescriptor) {
278 removeCallback: _prepareTmpFileRemoveCallback(name, discardOrDetachDescriptor ? -1 : fd, opts)
283 * Removes files and folders in a directory recursively.
285 * @param {string} root
288 function _rmdirRecursiveSync(root) {
295 files = fs.readdirSync(dir);
297 for (var i = 0, length = files.length; i < length; i++) {
299 file = path.join(dir, files[i]),
300 stat = fs.lstatSync(file); // lstat so we don't recurse into symlinked directories
302 if (stat.isDirectory()) {
316 } while (dirs.length !== 0);
320 * Creates a temporary directory.
322 * @param {(Options|dirCallback)} options the options or the callback function
323 * @param {?dirCallback} callback
325 function dir(options, callback) {
327 args = _parseArguments(options, callback),
331 // gets a temporary filename
332 tmpName(opts, function _tmpNameCreated(err, name) {
333 if (err) return cb(err);
335 // create the directory
336 fs.mkdir(name, opts.mode || DIR_MODE, function _dirCreated(err) {
337 if (err) return cb(err);
339 cb(null, name, _prepareTmpDirRemoveCallback(name, opts));
345 * Synchronous version of dir.
347 * @param {Options} options
348 * @returns {DirSyncObject} object consists of name and removeCallback
349 * @throws {Error} if it cannot create a directory
351 function dirSync(options) {
353 args = _parseArguments(options),
356 const name = tmpNameSync(opts);
357 fs.mkdirSync(name, opts.mode || DIR_MODE);
361 removeCallback: _prepareTmpDirRemoveCallback(name, opts)
366 * Prepares the callback for removal of the temporary file.
368 * @param {string} name the path of the file
369 * @param {number} fd file descriptor
370 * @param {Object} opts
371 * @returns {fileCallback}
374 function _prepareTmpFileRemoveCallback(name, fd, opts) {
375 const removeCallback = _prepareRemoveCallback(function _removeCallback(fdPath) {
377 if (0 <= fdPath[0]) {
378 fs.closeSync(fdPath[0]);
382 // under some node/windows related circumstances, a temporary file
383 // may have not be created as expected or the file was already closed
384 // by the user, in which case we will simply ignore the error
385 if (!isEBADF(e) && !isENOENT(e)) {
386 // reraise any unanticipated error
391 fs.unlinkSync(fdPath[1]);
395 // reraise any unanticipated error
402 _removeObjects.unshift(removeCallback);
405 return removeCallback;
409 * Prepares the callback for removal of the temporary directory.
411 * @param {string} name
412 * @param {Object} opts
413 * @returns {Function} the callback
416 function _prepareTmpDirRemoveCallback(name, opts) {
417 const removeFunction = opts.unsafeCleanup ? _rmdirRecursiveSync : fs.rmdirSync.bind(fs);
418 const removeCallback = _prepareRemoveCallback(removeFunction, name);
421 _removeObjects.unshift(removeCallback);
424 return removeCallback;
428 * Creates a guarded function wrapping the removeFunction call.
430 * @param {Function} removeFunction
431 * @param {Object} arg
432 * @returns {Function}
435 function _prepareRemoveCallback(removeFunction, arg) {
438 return function _cleanupCallback(next) {
440 const index = _removeObjects.indexOf(_cleanupCallback);
442 _removeObjects.splice(index, 1);
449 if (next) next(null);
454 * The garbage collector.
458 function _garbageCollector() {
459 if (_uncaughtException && !_gracefulCleanup) {
463 // the function being called removes itself from _removeObjects,
464 // loop until _removeObjects is empty
465 while (_removeObjects.length) {
467 _removeObjects[0].call(null);
475 * Helper for testing against EBADF to compensate changes made to Node 7.x under Windows.
477 function isEBADF(error) {
478 return isExpectedError(error, -EBADF, 'EBADF');
482 * Helper for testing against ENOENT to compensate changes made to Node 7.x under Windows.
484 function isENOENT(error) {
485 return isExpectedError(error, -ENOENT, 'ENOENT');
489 * Helper to determine whether the expected error code matches the actual code and errno,
490 * which will differ between the supported node versions.
493 * error.code {String}
494 * error.errno {String|Number} any numerical value will be negated
496 * - Node >= 6.0 < 7.0:
497 * error.code {String}
498 * error.errno {Number} negated
500 * - Node >= 4.0 < 6.0: introduces SystemError
501 * error.code {String}
502 * error.errno {Number} negated
504 * - Node >= 0.10 < 4.0:
505 * error.code {Number} negated
508 function isExpectedError(error, code, errno) {
509 return error.code == code || error.code == errno;
513 * Sets the graceful cleanup.
515 * Also removes the created files and directories when an uncaught exception occurs.
517 function setGracefulCleanup() {
518 _gracefulCleanup = true;
521 const version = process.versions.node.split('.').map(function (value) {
522 return parseInt(value, 10);
525 if (version[0] === 0 && (version[1] < 9 || version[1] === 9 && version[2] < 5)) {
526 process.addListener('uncaughtException', function _uncaughtExceptionThrown(err) {
527 _uncaughtException = true;
534 process.addListener('exit', function _exit(code) {
535 if (code) _uncaughtException = true;
540 * Configuration options.
542 * @typedef {Object} Options
543 * @property {?number} tries the number of tries before give up the name generation
544 * @property {?string} template the "mkstemp" like filename template
545 * @property {?string} name fix name
546 * @property {?string} dir the tmp directory to use
547 * @property {?string} prefix prefix for the generated name
548 * @property {?string} postfix postfix for the generated name
552 * @typedef {Object} FileSyncObject
553 * @property {string} name the name of the file
554 * @property {string} fd the file descriptor
555 * @property {fileCallback} removeCallback the callback function to remove the file
559 * @typedef {Object} DirSyncObject
560 * @property {string} name the name of the directory
561 * @property {fileCallback} removeCallback the callback function to remove the directory
565 * @callback tmpNameCallback
566 * @param {?Error} err the error object if anything goes wrong
567 * @param {string} name the temporary file name
571 * @callback fileCallback
572 * @param {?Error} err the error object if anything goes wrong
573 * @param {string} name the temporary file name
574 * @param {number} fd the file descriptor
575 * @param {cleanupCallback} fn the cleanup callback function
579 * @callback dirCallback
580 * @param {?Error} err the error object if anything goes wrong
581 * @param {string} name the temporary file name
582 * @param {cleanupCallback} fn the cleanup callback function
586 * Removes the temporary created file or directory.
588 * @callback cleanupCallback
589 * @param {simpleCallback} [next] function to call after entry was removed
593 * Callback function for function composition.
594 * @see {@link https://github.com/raszi/node-tmp/issues/57|raszi/node-tmp#57}
596 * @callback simpleCallback
599 // exporting all the needed methods
600 module.exports.tmpdir = tmpDir;
602 module.exports.dir = dir;
603 module.exports.dirSync = dirSync;
605 module.exports.file = file;
606 module.exports.fileSync = fileSync;
608 module.exports.tmpName = tmpName;
609 module.exports.tmpNameSync = tmpNameSync;
611 module.exports.setGracefulCleanup = setGracefulCleanup;