@@ -10,21 +10,18 @@
* Module dependencies.
*/
const fs = require('fs');
+const os = require('os');
const path = require('path');
const crypto = require('crypto');
-const osTmpDir = require('os-tmpdir');
-const _c = process.binding('constants');
+const _c = fs.constants && os.constants ?
+ { fs: fs.constants, os: os.constants } :
+ process.binding('constants');
+const rimraf = require('rimraf');
/*
* The working inner variables.
*/
const
- /**
- * The temporary directory.
- * @type {string}
- */
- tmpDir = osTmpDir(),
-
// the random characters to choose from
RANDOM_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
@@ -40,12 +37,15 @@
DIR_MODE = 448 /* 0o700 */,
FILE_MODE = 384 /* 0o600 */,
+ EXIT = 'exit',
+
+ SIGINT = 'SIGINT',
+
// this will hold the objects need to be removed on exit
_removeObjects = [];
var
- _gracefulCleanup = false,
- _uncaughtException = false;
+ _gracefulCleanup = false;
/**
* Random name generator based on crypto.
@@ -96,10 +96,12 @@
* @private
*/
function _parseArguments(options, callback) {
- if (typeof options == 'function') {
- return [callback || {}, options];
+ /* istanbul ignore else */
+ if (typeof options === 'function') {
+ return [{}, options];
}
+ /* istanbul ignore else */
if (_isUndefined(options)) {
return [{}, callback];
}
@@ -115,21 +117,37 @@
* @private
*/
function _generateTmpName(opts) {
- if (opts.name) {
+
+ const tmpDir = _getTmpDir();
+
+ // fail early on missing tmp dir
+ if (isBlank(opts.dir) && isBlank(tmpDir)) {
+ throw new Error('No tmp dir specified');
+ }
+
+ /* istanbul ignore else */
+ if (!isBlank(opts.name)) {
return path.join(opts.dir || tmpDir, opts.name);
}
// mkstemps like template
+ // opts.template has already been guarded in tmpName() below
+ /* istanbul ignore else */
if (opts.template) {
- return opts.template.replace(TEMPLATE_PATTERN, _randomChars(6));
+ var template = opts.template;
+ // make sure that we prepend the tmp path if none was given
+ /* istanbul ignore else */
+ if (path.basename(template) === template)
+ template = path.join(opts.dir || tmpDir, template);
+ return template.replace(TEMPLATE_PATTERN, _randomChars(6));
}
// prefix and postfix
const name = [
- opts.prefix || 'tmp-',
+ (isBlank(opts.prefix) ? 'tmp-' : opts.prefix),
process.pid,
_randomChars(12),
- opts.postfix || ''
+ (opts.postfix ? opts.postfix : '')
].join('');
return path.join(opts.dir || tmpDir, name);
@@ -146,20 +164,25 @@
args = _parseArguments(options, callback),
opts = args[0],
cb = args[1],
- tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
+ tries = !isBlank(opts.name) ? 1 : opts.tries || DEFAULT_TRIES;
+ /* istanbul ignore else */
if (isNaN(tries) || tries < 0)
return cb(new Error('Invalid tries'));
+ /* istanbul ignore else */
if (opts.template && !opts.template.match(TEMPLATE_PATTERN))
return cb(new Error('Invalid template provided'));
(function _getUniqueName() {
+ try {
const name = _generateTmpName(opts);
// check whether the path exists then retry if needed
fs.stat(name, function (err) {
+ /* istanbul ignore else */
if (!err) {
+ /* istanbul ignore else */
if (tries-- > 0) return _getUniqueName();
return cb(new Error('Could not get a unique tmp filename, max tries reached ' + name));
@@ -167,6 +190,9 @@
cb(null, name);
});
+ } catch (err) {
+ cb(err);
+ }
}());
}
@@ -181,11 +207,13 @@
var
args = _parseArguments(options),
opts = args[0],
- tries = opts.name ? 1 : opts.tries || DEFAULT_TRIES;
+ tries = !isBlank(opts.name) ? 1 : opts.tries || DEFAULT_TRIES;
+ /* istanbul ignore else */
if (isNaN(tries) || tries < 0)
throw new Error('Invalid tries');
+ /* istanbul ignore else */
if (opts.template && !opts.template.match(TEMPLATE_PATTERN))
throw new Error('Invalid template provided');
@@ -213,18 +241,19 @@
opts = args[0],
cb = args[1];
- opts.postfix = (_isUndefined(opts.postfix)) ? '.tmp' : opts.postfix;
-
// gets a temporary filename
tmpName(opts, function _tmpNameCreated(err, name) {
+ /* istanbul ignore else */
if (err) return cb(err);
// create and open the file
fs.open(name, CREATE_FLAGS, opts.mode || FILE_MODE, function _fileCreated(err, fd) {
+ /* istanbul ignore else */
if (err) return cb(err);
if (opts.discardDescriptor) {
return fs.close(fd, function _discardCallback(err) {
+ /* istanbul ignore else */
if (err) {
// Low probability, and the file exists, so this could be
// ignored. If it isn't we certainly need to unlink the
@@ -242,6 +271,7 @@
cb(null, name, undefined, _prepareTmpFileRemoveCallback(name, -1, opts));
});
}
+ /* istanbul ignore else */
if (opts.detachDescriptor) {
return cb(null, name, fd, _prepareTmpFileRemoveCallback(name, -1, opts));
}
@@ -262,11 +292,10 @@
args = _parseArguments(options),
opts = args[0];
- opts.postfix = opts.postfix || '.tmp';
-
const discardOrDetachDescriptor = opts.discardDescriptor || opts.detachDescriptor;
const name = tmpNameSync(opts);
var fd = fs.openSync(name, CREATE_FLAGS, opts.mode || FILE_MODE);
+ /* istanbul ignore else */
if (opts.discardDescriptor) {
fs.closeSync(fd);
fd = undefined;
@@ -280,43 +309,6 @@
}
/**
- * Removes files and folders in a directory recursively.
- *
- * @param {string} root
- * @private
- */
-function _rmdirRecursiveSync(root) {
- const dirs = [root];
-
- do {
- var
- dir = dirs.pop(),
- deferred = false,
- files = fs.readdirSync(dir);
-
- for (var i = 0, length = files.length; i < length; i++) {
- var
- file = path.join(dir, files[i]),
- stat = fs.lstatSync(file); // lstat so we don't recurse into symlinked directories
-
- if (stat.isDirectory()) {
- if (!deferred) {
- deferred = true;
- dirs.push(dir);
- }
- dirs.push(file);
- } else {
- fs.unlinkSync(file);
- }
- }
-
- if (!deferred) {
- fs.rmdirSync(dir);
- }
- } while (dirs.length !== 0);
-}
-
-/**
* Creates a temporary directory.
*
* @param {(Options|dirCallback)} options the options or the callback function
@@ -330,10 +322,12 @@
// gets a temporary filename
tmpName(opts, function _tmpNameCreated(err, name) {
+ /* istanbul ignore else */
if (err) return cb(err);
// create the directory
fs.mkdir(name, opts.mode || DIR_MODE, function _dirCreated(err) {
+ /* istanbul ignore else */
if (err) return cb(err);
cb(null, name, _prepareTmpDirRemoveCallback(name, opts));
@@ -363,49 +357,95 @@
}
/**
- * Prepares the callback for removal of the temporary file.
+ * Removes files asynchronously.
*
- * @param {string} name the path of the file
- * @param {number} fd file descriptor
- * @param {Object} opts
- * @returns {fileCallback}
+ * @param {Object} fdPath
+ * @param {Function} next
* @private
*/
-function _prepareTmpFileRemoveCallback(name, fd, opts) {
- const removeCallback = _prepareRemoveCallback(function _removeCallback(fdPath) {
- try {
- if (0 <= fdPath[0]) {
- fs.closeSync(fdPath[0]);
- }
- }
- catch (e) {
- // under some node/windows related circumstances, a temporary file
- // may have not be created as expected or the file was already closed
- // by the user, in which case we will simply ignore the error
- if (!isEBADF(e) && !isENOENT(e)) {
+function _removeFileAsync(fdPath, next) {
+ const _handler = function (err) {
+ if (err && !isENOENT(err)) {
// reraise any unanticipated error
- throw e;
+ return next(err);
}
+ next();
}
+
+ if (0 <= fdPath[0])
+ fs.close(fdPath[0], function (err) {
+ fs.unlink(fdPath[1], _handler);
+ });
+ else fs.unlink(fdPath[1], _handler);
+}
+
+/**
+ * Removes files synchronously.
+ *
+ * @param {Object} fdPath
+ * @private
+ */
+function _removeFileSync(fdPath) {
+ try {
+ if (0 <= fdPath[0]) fs.closeSync(fdPath[0]);
+ } catch (e) {
+ // reraise any unanticipated error
+ if (!isEBADF(e) && !isENOENT(e)) throw e;
+ } finally {
try {
fs.unlinkSync(fdPath[1]);
}
catch (e) {
- if (!isENOENT(e)) {
// reraise any unanticipated error
- throw e;
+ if (!isENOENT(e)) throw e;
}
}
- }, [fd, name]);
+}
- if (!opts.keep) {
- _removeObjects.unshift(removeCallback);
- }
+/**
+ * Prepares the callback for removal of the temporary file.
+ *
+ * @param {string} name the path of the file
+ * @param {number} fd file descriptor
+ * @param {Object} opts
+ * @returns {fileCallback}
+ * @private
+ */
+function _prepareTmpFileRemoveCallback(name, fd, opts) {
+ const removeCallbackSync = _prepareRemoveCallback(_removeFileSync, [fd, name]);
+ const removeCallback = _prepareRemoveCallback(_removeFileAsync, [fd, name], removeCallbackSync);
+
+ if (!opts.keep) _removeObjects.unshift(removeCallbackSync);
return removeCallback;
}
/**
+ * Simple wrapper for rimraf.
+ *
+ * @param {string} dirPath
+ * @param {Function} next
+ * @private
+ */
+function _rimrafRemoveDirWrapper(dirPath, next) {
+ rimraf(dirPath, next);
+}
+
+/**
+ * Simple wrapper for rimraf.sync.
+ *
+ * @param {string} dirPath
+ * @private
+ */
+function _rimrafRemoveDirSyncWrapper(dirPath, next) {
+ try {
+ return next(null, rimraf.sync(dirPath));
+ } catch (err) {
+ return next(err);
+ }
+}
+
+/**
* Prepares the callback for removal of the temporary directory.
*
* @param {string} name
@@ -414,12 +454,11 @@
* @private
*/
function _prepareTmpDirRemoveCallback(name, opts) {
- const removeFunction = opts.unsafeCleanup ? _rmdirRecursiveSync : fs.rmdirSync.bind(fs);
- const removeCallback = _prepareRemoveCallback(removeFunction, name);
-
- if (!opts.keep) {
- _removeObjects.unshift(removeCallback);
- }
+ const removeFunction = opts.unsafeCleanup ? _rimrafRemoveDirWrapper : fs.rmdir.bind(fs);
+ const removeFunctionSync = opts.unsafeCleanup ? _rimrafRemoveDirSyncWrapper : fs.rmdirSync.bind(fs);
+ const removeCallbackSync = _prepareRemoveCallback(removeFunctionSync, name);
+ const removeCallback = _prepareRemoveCallback(removeFunction, name, removeCallbackSync);
+ if (!opts.keep) _removeObjects.unshift(removeCallbackSync);
return removeCallback;
}
@@ -432,21 +471,32 @@
* @returns {Function}
* @private
*/
-function _prepareRemoveCallback(removeFunction, arg) {
+function _prepareRemoveCallback(removeFunction, arg, cleanupCallbackSync) {
var called = false;
return function _cleanupCallback(next) {
+ next = next || function () {};
if (!called) {
- const index = _removeObjects.indexOf(_cleanupCallback);
- if (index >= 0) {
- _removeObjects.splice(index, 1);
- }
+ const toRemove = cleanupCallbackSync || _cleanupCallback;
+ const index = _removeObjects.indexOf(toRemove);
+ /* istanbul ignore else */
+ if (index >= 0) _removeObjects.splice(index, 1);
called = true;
+ // sync?
+ if (removeFunction.length === 1) {
+ try {
removeFunction(arg);
+ return next(null);
}
-
- if (next) next(null);
+ catch (err) {
+ // if no next is provided and since we are
+ // in silent cleanup mode on process exit,
+ // we will ignore the error
+ return next(err);
+ }
+ } else return removeFunction(arg, next);
+ } else return next(new Error('cleanup callback has already been called'));
};
}
@@ -456,15 +506,14 @@
* @private
*/
function _garbageCollector() {
- if (_uncaughtException && !_gracefulCleanup) {
- return;
- }
+ /* istanbul ignore else */
+ if (!_gracefulCleanup) return;
// the function being called removes itself from _removeObjects,
// loop until _removeObjects is empty
while (_removeObjects.length) {
try {
- _removeObjects[0].call(null);
+ _removeObjects[0]();
} catch (e) {
// already removed?
}
@@ -490,51 +539,143 @@
* which will differ between the supported node versions.
*
* - Node >= 7.0:
- * error.code {String}
- * error.errno {String|Number} any numerical value will be negated
+ * error.code {string}
+ * error.errno {string|number} any numerical value will be negated
*
* - Node >= 6.0 < 7.0:
- * error.code {String}
- * error.errno {Number} negated
+ * error.code {string}
+ * error.errno {number} negated
*
* - Node >= 4.0 < 6.0: introduces SystemError
- * error.code {String}
- * error.errno {Number} negated
+ * error.code {string}
+ * error.errno {number} negated
*
* - Node >= 0.10 < 4.0:
- * error.code {Number} negated
+ * error.code {number} negated
* error.errno n/a
*/
function isExpectedError(error, code, errno) {
- return error.code == code || error.code == errno;
+ return error.code === code || error.code === errno;
}
/**
- * Sets the graceful cleanup.
+ * Helper which determines whether a string s is blank, that is undefined, or empty or null.
*
- * Also removes the created files and directories when an uncaught exception occurs.
+ * @private
+ * @param {string} s
+ * @returns {Boolean} true whether the string s is blank, false otherwise
+ */
+function isBlank(s) {
+ return s === null || s === undefined || !s.trim();
+}
+
+/**
+ * Sets the graceful cleanup.
*/
function setGracefulCleanup() {
_gracefulCleanup = true;
}
-const version = process.versions.node.split('.').map(function (value) {
- return parseInt(value, 10);
-});
+/**
+ * Returns the currently configured tmp dir from os.tmpdir().
+ *
+ * @private
+ * @returns {string} the currently configured tmp dir
+ */
+function _getTmpDir() {
+ return os.tmpdir();
+}
-if (version[0] === 0 && (version[1] < 9 || version[1] === 9 && version[2] < 5)) {
- process.addListener('uncaughtException', function _uncaughtExceptionThrown(err) {
- _uncaughtException = true;
- _garbageCollector();
+/**
+ * If there are multiple different versions of tmp in place, make sure that
+ * we recognize the old listeners.
+ *
+ * @param {Function} listener
+ * @private
+ * @returns {Boolean} true whether listener is a legacy listener
+ */
+function _is_legacy_listener(listener) {
+ return (listener.name === '_exit' || listener.name === '_uncaughtExceptionThrown')
+ && listener.toString().indexOf('_garbageCollector();') > -1;
+}
+
+/**
+ * Safely install SIGINT listener.
+ *
+ * NOTE: this will only work on OSX and Linux.
+ *
+ * @private
+ */
+function _safely_install_sigint_listener() {
- throw err;
+ const listeners = process.listeners(SIGINT);
+ const existingListeners = [];
+ for (let i = 0, length = listeners.length; i < length; i++) {
+ const lstnr = listeners[i];
+ /* istanbul ignore else */
+ if (lstnr.name === '_tmp$sigint_listener') {
+ existingListeners.push(lstnr);
+ process.removeListener(SIGINT, lstnr);
+ }
+ }
+ process.on(SIGINT, function _tmp$sigint_listener(doExit) {
+ for (let i = 0, length = existingListeners.length; i < length; i++) {
+ // let the existing listener do the garbage collection (e.g. jest sandbox)
+ try {
+ existingListeners[i](false);
+ } catch (err) {
+ // ignore
+ }
+ }
+ try {
+ // force the garbage collector even it is called again in the exit listener
+ _garbageCollector();
+ } finally {
+ if (!!doExit) {
+ process.exit(0);
+ }
+ }
});
}
-process.addListener('exit', function _exit(code) {
- if (code) _uncaughtException = true;
+/**
+ * Safely install process exit listener.
+ *
+ * @private
+ */
+function _safely_install_exit_listener() {
+ const listeners = process.listeners(EXIT);
+
+ // collect any existing listeners
+ const existingListeners = [];
+ for (let i = 0, length = listeners.length; i < length; i++) {
+ const lstnr = listeners[i];
+ /* istanbul ignore else */
+ // TODO: remove support for legacy listeners once release 1.0.0 is out
+ if (lstnr.name === '_tmp$safe_listener' || _is_legacy_listener(lstnr)) {
+ // we must forget about the uncaughtException listener, hopefully it is ours
+ if (lstnr.name !== '_uncaughtExceptionThrown') {
+ existingListeners.push(lstnr);
+ }
+ process.removeListener(EXIT, lstnr);
+ }
+ }
+ // TODO: what was the data parameter good for?
+ process.addListener(EXIT, function _tmp$safe_listener(data) {
+ for (let i = 0, length = existingListeners.length; i < length; i++) {
+ // let the existing listener do the garbage collection (e.g. jest sandbox)
+ try {
+ existingListeners[i](data);
+ } catch (err) {
+ // ignore
+ }
+ }
_garbageCollector();
-});
+ });
+}
+
+_safely_install_exit_listener();
+_safely_install_sigint_listener();
/**
* Configuration options.
@@ -546,6 +687,7 @@
* @property {?string} dir the tmp directory to use
* @property {?string} prefix prefix for the generated name
* @property {?string} postfix postfix for the generated name
+ * @property {?boolean} unsafeCleanup recursively removes the created temporary directory, even when it's not empty
*/
/**
@@ -597,7 +739,16 @@
*/
// exporting all the needed methods
-module.exports.tmpdir = tmpDir;
+
+// evaluate os.tmpdir() lazily, mainly for simplifying testing but it also will
+// allow users to reconfigure the temporary directory
+Object.defineProperty(module.exports, 'tmpdir', {
+ enumerable: true,
+ configurable: false,
+ get: function () {
+ return _getTmpDir();
+ }
+});
module.exports.dir = dir;
module.exports.dirSync = dirSync;