463 lines
10 KiB
JavaScript
463 lines
10 KiB
JavaScript
/*!
|
|
* Tmp
|
|
*
|
|
* Copyright (c) 2011-2015 KARASZI Istvan <github@spam.raszi.hu>
|
|
*
|
|
* MIT Licensed
|
|
*/
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
var
|
|
fs = require('fs'),
|
|
path = require('path'),
|
|
os = require('os'),
|
|
crypto = require('crypto'),
|
|
exists = fs.exists || path.exists,
|
|
existsSync = fs.existsSync || path.existsSync,
|
|
tmpDir = require('os-tmpdir'),
|
|
_c = require('constants');
|
|
|
|
|
|
/**
|
|
* The working inner variables.
|
|
*/
|
|
var
|
|
// store the actual TMP directory
|
|
_TMP = tmpDir(),
|
|
|
|
// the random characters to choose from
|
|
RANDOM_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
|
|
TEMPLATE_PATTERN = /XXXXXX/,
|
|
|
|
DEFAULT_TRIES = 3,
|
|
|
|
CREATE_FLAGS = _c.O_CREAT | _c.O_EXCL | _c.O_RDWR,
|
|
|
|
DIR_MODE = 448 /* 0700 */,
|
|
FILE_MODE = 384 /* 0600 */,
|
|
|
|
// this will hold the objects need to be removed on exit
|
|
_removeObjects = [],
|
|
|
|
_gracefulCleanup = false,
|
|
_uncaughtException = false;
|
|
|
|
/**
|
|
* Random name generator based on crypto.
|
|
* Adapted from http://blog.tompawlak.org/how-to-generate-random-values-nodejs-javascript
|
|
*
|
|
* @param {Number} howMany
|
|
* @return {String}
|
|
* @api private
|
|
*/
|
|
function _randomChars(howMany) {
|
|
var
|
|
value = [],
|
|
rnd = null;
|
|
|
|
// make sure that we do not fail because we ran out of entropy
|
|
try {
|
|
rnd = crypto.randomBytes(howMany);
|
|
} catch (e) {
|
|
rnd = crypto.pseudoRandomBytes(howMany);
|
|
}
|
|
|
|
for (var i = 0; i < howMany; i++) {
|
|
value.push(RANDOM_CHARS[rnd[i] % RANDOM_CHARS.length]);
|
|
}
|
|
|
|
return value.join('');
|
|
}
|
|
|
|
/**
|
|
* Checks whether the `obj` parameter is defined or not.
|
|
*
|
|
* @param {Object} obj
|
|
* @return {Boolean}
|
|
* @api private
|
|
*/
|
|
function _isUndefined(obj) {
|
|
return typeof obj === 'undefined';
|
|
}
|
|
|
|
/**
|
|
* Parses the function arguments.
|
|
*
|
|
* This function helps to have optional arguments.
|
|
*
|
|
* @param {Object} options
|
|
* @param {Function} callback
|
|
* @api private
|
|
*/
|
|
function _parseArguments(options, callback) {
|
|
if (typeof options == 'function') {
|
|
var
|
|
tmp = options;
|
|
options = callback || {};
|
|
callback = tmp;
|
|
} else if (typeof options == 'undefined') {
|
|
options = {};
|
|
}
|
|
|
|
return [options, callback];
|
|
}
|
|
|
|
/**
|
|
* Generates a new temporary name.
|
|
*
|
|
* @param {Object} opts
|
|
* @returns {String}
|
|
* @api private
|
|
*/
|
|
function _generateTmpName(opts) {
|
|
if (opts.name) {
|
|
return path.join(opts.dir || _TMP, opts.name);
|
|
}
|
|
|
|
// mkstemps like template
|
|
if (opts.template) {
|
|
return opts.template.replace(TEMPLATE_PATTERN, _randomChars(6));
|
|
}
|
|
|
|
// prefix and postfix
|
|
var name = [
|
|
opts.prefix || 'tmp-',
|
|
process.pid,
|
|
_randomChars(12),
|
|
opts.postfix || ''
|
|
].join('');
|
|
|
|
return path.join(opts.dir || _TMP, name);
|
|
}
|
|
|
|
/**
|
|
* Gets a temporary file name.
|
|
*
|
|
* @param {Object} options
|
|
* @param {Function} callback
|
|
* @api private
|
|
*/
|
|
function _getTmpName(options, callback) {
|
|
var
|
|
args = _parseArguments(options, callback),
|
|
opts = args[0],
|
|
cb = args[1],
|
|
tries = opts.tries || DEFAULT_TRIES;
|
|
|
|
if (isNaN(tries) || tries < 0)
|
|
return cb(new Error('Invalid tries'));
|
|
|
|
if (opts.template && !opts.template.match(TEMPLATE_PATTERN))
|
|
return cb(new Error('Invalid template provided'));
|
|
|
|
(function _getUniqueName() {
|
|
var name = _generateTmpName(opts);
|
|
|
|
// check whether the path exists then retry if needed
|
|
exists(name, function _pathExists(pathExists) {
|
|
if (pathExists) {
|
|
if (tries-- > 0) return _getUniqueName();
|
|
|
|
return cb(new Error('Could not get a unique tmp filename, max tries reached ' + name));
|
|
}
|
|
|
|
cb(null, name);
|
|
});
|
|
}());
|
|
}
|
|
|
|
/**
|
|
* Synchronous version of _getTmpName.
|
|
*
|
|
* @param {Object} options
|
|
* @returns {String}
|
|
* @api private
|
|
*/
|
|
function _getTmpNameSync(options) {
|
|
var
|
|
args = _parseArguments(options),
|
|
opts = args[0],
|
|
tries = opts.tries || DEFAULT_TRIES;
|
|
|
|
if (isNaN(tries) || tries < 0)
|
|
throw new Error('Invalid tries');
|
|
|
|
if (opts.template && !opts.template.match(TEMPLATE_PATTERN))
|
|
throw new Error('Invalid template provided');
|
|
|
|
do {
|
|
var name = _generateTmpName(opts);
|
|
if (!existsSync(name)) {
|
|
return name;
|
|
}
|
|
} while (tries-- > 0);
|
|
|
|
throw new Error('Could not get a unique tmp filename, max tries reached');
|
|
}
|
|
|
|
/**
|
|
* Creates and opens a temporary file.
|
|
*
|
|
* @param {Object} options
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
function _createTmpFile(options, callback) {
|
|
var
|
|
args = _parseArguments(options, callback),
|
|
opts = args[0],
|
|
cb = args[1];
|
|
|
|
opts.postfix = (_isUndefined(opts.postfix)) ? '.tmp' : opts.postfix;
|
|
|
|
// gets a temporary filename
|
|
_getTmpName(opts, function _tmpNameCreated(err, name) {
|
|
if (err) return cb(err);
|
|
|
|
// create and open the file
|
|
fs.open(name, CREATE_FLAGS, opts.mode || FILE_MODE, function _fileCreated(err, fd) {
|
|
if (err) return cb(err);
|
|
|
|
cb(null, name, fd, _prepareTmpFileRemoveCallback(name, fd, opts));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Synchronous version of _createTmpFile.
|
|
*
|
|
* @param {Object} options
|
|
* @returns {Object} object consists of name, fd and removeCallback
|
|
* @api private
|
|
*/
|
|
function _createTmpFileSync(options) {
|
|
var
|
|
args = _parseArguments(options),
|
|
opts = args[0];
|
|
|
|
opts.postfix = opts.postfix || '.tmp';
|
|
|
|
var name = _getTmpNameSync(opts);
|
|
var fd = fs.openSync(name, CREATE_FLAGS, opts.mode || FILE_MODE);
|
|
|
|
return {
|
|
name : name,
|
|
fd : fd,
|
|
removeCallback : _prepareTmpFileRemoveCallback(name, fd, opts)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Removes files and folders in a directory recursively.
|
|
*
|
|
* @param {String} root
|
|
* @api private
|
|
*/
|
|
function _rmdirRecursiveSync(root) {
|
|
var 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 {Object} options
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
function _createTmpDir(options, callback) {
|
|
var
|
|
args = _parseArguments(options, callback),
|
|
opts = args[0],
|
|
cb = args[1];
|
|
|
|
// gets a temporary filename
|
|
_getTmpName(opts, function _tmpNameCreated(err, name) {
|
|
if (err) return cb(err);
|
|
|
|
// create the directory
|
|
fs.mkdir(name, opts.mode || DIR_MODE, function _dirCreated(err) {
|
|
if (err) return cb(err);
|
|
|
|
cb(null, name, _prepareTmpDirRemoveCallback(name, opts));
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Synchronous version of _createTmpDir.
|
|
*
|
|
* @param {Object} options
|
|
* @returns {Object} object consists of name and removeCallback
|
|
* @api private
|
|
*/
|
|
function _createTmpDirSync(options) {
|
|
var
|
|
args = _parseArguments(options),
|
|
opts = args[0];
|
|
|
|
var name = _getTmpNameSync(opts);
|
|
fs.mkdirSync(name, opts.mode || DIR_MODE);
|
|
|
|
return {
|
|
name : name,
|
|
removeCallback : _prepareTmpDirRemoveCallback(name, opts)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Prepares the callback for removal of the temporary file.
|
|
*
|
|
* @param {String} name
|
|
* @param {int} fd
|
|
* @param {Object} opts
|
|
* @api private
|
|
* @returns {Function} the callback
|
|
*/
|
|
function _prepareTmpFileRemoveCallback(name, fd, opts) {
|
|
var removeCallback = _prepareRemoveCallback(function _removeCallback(fdPath) {
|
|
try {
|
|
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 (e.errno != -_c.EBADF && e.errno != -c.ENOENT) {
|
|
// reraise any unanticipated error
|
|
throw e;
|
|
}
|
|
}
|
|
fs.unlinkSync(fdPath[1]);
|
|
}, [fd, name]);
|
|
|
|
if (!opts.keep) {
|
|
_removeObjects.unshift(removeCallback);
|
|
}
|
|
|
|
return removeCallback;
|
|
}
|
|
|
|
/**
|
|
* Prepares the callback for removal of the temporary directory.
|
|
*
|
|
* @param {String} name
|
|
* @param {Object} opts
|
|
* @returns {Function} the callback
|
|
* @api private
|
|
*/
|
|
function _prepareTmpDirRemoveCallback(name, opts) {
|
|
var removeFunction = opts.unsafeCleanup ? _rmdirRecursiveSync : fs.rmdirSync.bind(fs);
|
|
var removeCallback = _prepareRemoveCallback(removeFunction, name);
|
|
|
|
if (!opts.keep) {
|
|
_removeObjects.unshift(removeCallback);
|
|
}
|
|
|
|
return removeCallback;
|
|
}
|
|
|
|
/**
|
|
* Creates a guarded function wrapping the removeFunction call.
|
|
*
|
|
* @param {Function} removeFunction
|
|
* @param {Object} arg
|
|
* @returns {Function}
|
|
* @api private
|
|
*/
|
|
function _prepareRemoveCallback(removeFunction, arg) {
|
|
var called = false;
|
|
|
|
return function _cleanupCallback() {
|
|
if (called) return;
|
|
|
|
var index = _removeObjects.indexOf(removeFunction);
|
|
if (index >= 0) {
|
|
_removeObjects.splice(index, 1);
|
|
}
|
|
|
|
called = true;
|
|
removeFunction(arg);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The garbage collector.
|
|
*
|
|
* @api private
|
|
*/
|
|
function _garbageCollector() {
|
|
if (_uncaughtException && !_gracefulCleanup) {
|
|
return;
|
|
}
|
|
|
|
for (var i = 0, length = _removeObjects.length; i < length; i++) {
|
|
try {
|
|
_removeObjects[i].call(null);
|
|
} catch (e) {
|
|
// already removed?
|
|
}
|
|
}
|
|
}
|
|
|
|
function _setGracefulCleanup() {
|
|
_gracefulCleanup = true;
|
|
}
|
|
|
|
var version = process.versions.node.split('.').map(function (value) {
|
|
return parseInt(value, 10);
|
|
});
|
|
|
|
if (version[0] === 0 && (version[1] < 9 || version[1] === 9 && version[2] < 5)) {
|
|
process.addListener('uncaughtException', function _uncaughtExceptionThrown(err) {
|
|
_uncaughtException = true;
|
|
_garbageCollector();
|
|
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
process.addListener('exit', function _exit(code) {
|
|
if (code) _uncaughtException = true;
|
|
_garbageCollector();
|
|
});
|
|
|
|
// exporting all the needed methods
|
|
module.exports.tmpdir = _TMP;
|
|
module.exports.dir = _createTmpDir;
|
|
module.exports.dirSync = _createTmpDirSync;
|
|
module.exports.file = _createTmpFile;
|
|
module.exports.fileSync = _createTmpFileSync;
|
|
module.exports.tmpName = _getTmpName;
|
|
module.exports.tmpNameSync = _getTmpNameSync;
|
|
module.exports.setGracefulCleanup = _setGracefulCleanup;
|