UNPKG

actionhero

Version:

actionhero.js is a multi-transport API Server with integrated cluster capabilities and delayed tasks

474 lines (434 loc) 16.4 kB
'use strict' const fs = require('fs') const path = require('path') const async = require('async') const dotProp = require('dot-prop') /** * Utilites for any ActionHero project. * * @namespace api.utils * @property {Object} dotProp - The dotProp package. */ module.exports = { loadPriority: 0, initialize: function (api, next) { if (!api.utils) { api.utils = {} } api.utils.dotProp = dotProp /** * Recursivley merge 2 Objects together. Will resolve functions if they are present, unless the parent Object has the propery `_toExpand = false`. * ActionHero uses this internally to construct and resolve the config. * Matching keys in B override A. * * @param {Object} a Object 1 * @param {Object} b Object 2 * @param {Object} arg Arguments to pass to any functiosn which should be resolved. * @return {Object} A new Object, combining A and B */ api.utils.hashMerge = function (a, b, arg) { let c = {} let i let response for (i in a) { if (api.utils.isPlainObject(a[i]) && Object.keys(a[i]).length > 0) { c[i] = api.utils.hashMerge(c[i], a[i], arg) } else { if (typeof a[i] === 'function') { response = a[i](arg) if (api.utils.isPlainObject(response)) { c[i] = api.utils.hashMerge(c[i], response, arg) } else { c[i] = response } } else { c[i] = a[i] } } } for (i in b) { if (api.utils.isPlainObject(b[i]) && Object.keys(b[i]).length > 0) { c[i] = api.utils.hashMerge(c[i], b[i], arg) } else { if (typeof b[i] === 'function') { response = b[i](arg) if (api.utils.isPlainObject(response)) { c[i] = api.utils.hashMerge(c[i], response, arg) } else { c[i] = response } } else { c[i] = b[i] } } } return c } api.utils.isPlainObject = function (o) { const safeTypes = [Boolean, Number, String, Function, Array, Date, RegExp, Buffer] const safeInstances = ['boolean', 'number', 'string', 'function'] const expandPreventMatchKey = '_toExpand' // set `_toExpand = false` within an object if you don't want to expand it let i if (!o) { return false } if ((o instanceof Object) === false) { return false } for (i in safeTypes) { if (o instanceof safeTypes[i]) { return false } } for (i in safeInstances) { if (typeof o === safeInstances[i]) { return false } //eslint-disable-line } if (o[expandPreventMatchKey] === false) { return false } return (o.toString() === '[object Object]') } /** * Return only the unique values in an Array. * * @param {Array} arr Source Array. * @return {Array} Unique Array. */ api.utils.arrayUniqueify = function (arr) { let a = [] for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { if (arr[i] === arr[j]) { j = ++i } } a.push(arr[i]) } return a } // ////////////////////////////////////////////////////////////////////////// // get all .js files in a directory api.utils.recursiveDirectoryGlob = function (dir, extension, followLinkFiles) { let results = [] if (!extension) { extension = '.js' } if (!followLinkFiles) { followLinkFiles = true } extension = extension.replace('.', '') if (fs.existsSync(dir)) { fs.readdirSync(dir).forEach((file) => { let fullFilePath = path.join(dir, file) if (file[0] !== '.') { // ignore 'system' files let stats = fs.statSync(fullFilePath) let child if (stats.isDirectory()) { child = api.utils.recursiveDirectoryGlob(fullFilePath, extension, followLinkFiles) child.forEach((c) => { results.push(c) }) } else if (stats.isSymbolicLink()) { let realPath = fs.readlinkSync(fullFilePath) child = api.utils.recursiveDirectoryGlob(realPath, extension, followLinkFiles) child.forEach((c) => { results.push(c) }) } else if (stats.isFile()) { let fileParts = file.split('.') let ext = fileParts[(fileParts.length - 1)] // real file match if (ext === extension) { results.push(fullFilePath) } // linkfile traversal if (ext === 'link' && followLinkFiles === true) { let linkedPath = api.utils.sourceRelativeLinkPath(fullFilePath, api.config.general.paths.plugin) if (linkedPath) { child = api.utils.recursiveDirectoryGlob(linkedPath, extension, followLinkFiles) child.forEach((c) => { results.push(c) }) } else { try { api.log(`cannot find linked refrence to \`${file}\``, 'warning') } catch (e) { throw new Error('cannot find linked refrence to ' + file) } } } } } }) } return results.sort() } /** * @private */ api.utils.sourceRelativeLinkPath = function (linkfile, pluginPaths) { const type = fs.readFileSync(linkfile).toString() const pathParts = linkfile.split(path.sep) const name = pathParts[(pathParts.length - 1)].split('.')[0] const pathsToTry = pluginPaths.slice(0) let pluginRoot // TODO: always also try the local destination's `node_modules` to allow for nested plugins // This might be a security risk without requiring explicit sourcing pathsToTry.forEach((pluginPath) => { let pluginPathAttempt = path.normalize(pluginPath + path.sep + name) try { let stats = fs.lstatSync(pluginPathAttempt) if (!pluginRoot && (stats.isDirectory() || stats.isSymbolicLink())) { pluginRoot = pluginPathAttempt } } catch (e) { } }) if (!pluginRoot) { return false } let pluginSection = path.normalize(pluginRoot + path.sep + type) return pluginSection } // ////////////////////////////////////////////////////////////////////////// // object Clone api.utils.objClone = function (obj) { return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyNames(obj).reduce((memo, name) => { return (memo[name] = Object.getOwnPropertyDescriptor(obj, name)) && memo }, {})) } /** * Collapsses an Object with numerical keys (like `arguments` in a function) to an Array * * @param {Object} obj An Object with a depth of 1 and only Numerical keys * @return {Array} Array */ api.utils.collapseObjectToArray = function (obj) { try { const keys = Object.keys(obj) if (keys.length < 1) { return false } if (keys[0] !== '0') { return false } if (keys[(keys.length - 1)] !== String(keys.length - 1)) { return false } let arr = [] for (let i in keys) { let key = keys[i] if (String(parseInt(key)) !== key) { return false } else { arr.push(obj[key]) } } return arr } catch (e) { return false } } /** * Returns this server's external/public IP address * * @return {string} This server's external IP address. */ api.utils.getExternalIPAddress = function () { const os = require('os') const ifaces = os.networkInterfaces() let ip = false for (let dev in ifaces) { ifaces[dev].forEach((details) => { if (details.family === 'IPv4' && details.address !== '127.0.0.1') { ip = details.address } }) } return ip } /** * Transform the cookie headers of a node HTTP `req` Object into a hash. * * @param {Object} req A node.js `req` Object * @return {Object} A Object with Cookies. */ api.utils.parseCookies = function (req) { let cookies = {} if (req.headers.cookie) { req.headers.cookie.split(';').forEach((cookie) => { let parts = cookie.split('=') cookies[parts[0].trim()] = (parts[1] || '').trim() }) } return cookies } /** * Parse an IPv6 address, returning both host and port. * * @param {string} addr An IPv6 address. * @return {Object} An Object with {host, port} * @see https://github.com/actionhero/actionhero/issues/275 */ api.utils.parseIPv6URI = function (addr) { let host = '::1' let port = '80' let regexp = new RegExp(/\[([0-9a-f:]+)]:([0-9]{1,5})/) // if we have brackets parse them and find a port if (addr.indexOf('[') > -1 && addr.indexOf(']') > -1) { let res = regexp.exec(addr) if (res === null) { throw new Error('failed to parse address') } host = res[1] port = res[2] } else { host = addr } return {host: host, port: parseInt(port, 10)} } /** * Returns the averge delay in ms between a tick of hte node.js event loop, as measured for N calls of `process.nextTick` * * @param {Number} itterations How many `process.nextTick` cycles of the event loop should we measure? * @param {numberCallback} callback The callback to handle the response. */ api.utils.eventLoopDelay = function (itterations, callback) { let intervalJobs = [] let intervalTimes = [] if (!itterations) { return callback(new Error('itterations is required')) } let i = 0 while (i < itterations) { intervalJobs.push((intervalDone) => { let start = process.hrtime() process.nextTick(() => { let delta = process.hrtime(start) let ms = (delta[0] * 1000) + (delta[1] / 1000000) intervalTimes.push(ms) intervalDone() }) }) i++ } async.series(intervalJobs, function () { let sum = 0 intervalTimes.forEach((t) => { sum += t }) let avg = Math.round(sum / intervalTimes.length * 10000) / 1000 return callback(null, avg) }) } /** * Sorts an Array of Objects with a priority key * * @param {Array} globalMiddlewareList The Array to sort. * @param {Array} middleware A specific collection to sort against. */ api.utils.sortGlobalMiddleware = function (globalMiddlewareList, middleware) { globalMiddlewareList.sort((a, b) => { if (middleware[a].priority > middleware[b].priority) { return 1 } else { return -1 } }) } /** * Check if a directory exists. * * @param {string} dir The directory to check. * @return {Boolean} */ api.utils.dirExists = function (dir) { try { let stats = fs.lstatSync(dir) return (stats.isDirectory() || stats.isSymbolicLink()) } catch (e) { return false } } /** * Check if a file exists. * * @param {string} file The file to check. * @return {Boolean} */ api.utils.fileExists = function (file) { try { let stats = fs.lstatSync(file) return (stats.isFile() || stats.isSymbolicLink()) } catch (e) { return false } } /** * Create a directory, only if it doesn't exist yet. * Throws an error if the directory already exists, or encounters a filesystem problem. * * @param {string} dir The directory to create. * @return {string} a message if the file was created to log. */ api.utils.createDirSafely = function (dir) { if (api.utils.dirExists(dir)) { api.log(` - directory '${path.normalize(dir)}' already exists, skipping`, 'alert') } else { api.log(` - creating directory '${path.normalize(dir)}'`) fs.mkdirSync(path.normalize(dir), '0766') } } /** * Create a file, only if it doesn't exist yet. * Throws an error if the file already exists, or encounters a filesystem problem. * * @param {string} file The file to create. * @param {string} data The new contents of the file. * @param {boolean} overwrite Should we overwrite an existing file?. * @return {string} a message if the file was created to log. */ api.utils.createFileSafely = function (file, data, overwrite) { if (api.utils.fileExists(file) && !overwrite) { api.log(` - file '${path.normalize(file)}' already exists, skipping`, 'alert') } else { if (overwrite && api.utils.fileExists(file)) { api.log(` - overwritten file '${path.normalize(file)}'`) } else { api.log(` - wrote file '${path.normalize(file)}'`) } fs.writeFileSync(path.normalize(file), data) } } /** * Create an ActionHero LinkFile, only if it doesn't exist yet. * Throws an error if the file already exists, or encounters a filesystem problem. * * @param {string} filePath The path of the new LinkFile * @param {string} type What are we linking (actions, tasks, etc). * @param {string} refrence What we are refrencing via this link. * @return {string} a message if the file was created to log. */ api.utils.createLinkfileSafely = function (filePath, type, refrence) { if (api.utils.fileExists(filePath)) { api.log(` - link file '${filePath}' already exists, skipping`, 'alert') } else { api.log(` - creating linkfile '${filePath}'`) fs.writeFileSync(filePath, type) } } /** * Remove an ActionHero LinkFile, only if it exists. * Throws an error if the file does not exist, or encounters a filesystem problem. * * @param {string} filePath The path of the LinkFile * @param {string} type What are we linking (actions, tasks, etc). * @param {string} refrence What we are refrencing via this link. * @return {string} a message if the file was created to log. */ api.utils.removeLinkfileSafely = function (filePath, type, refrence) { if (!api.utils.fileExists(filePath)) { api.log(` - link file '${filePath}' doesn't exist, skipping`, 'alert') } else { api.log(` - removing linkfile '${filePath}'`) fs.unlinkSync(filePath) } } /** * Create a system symbolic link. * Throws an error if it encounters a filesystem problem. * * @param {string} destination * @param {string} source * @return {string} a message if the symlionk was created to log. */ api.utils.createSymlinkSafely = function (destination, source) { if (api.utils.dirExists(destination)) { api.log(` - symbolic link '${destination}' already exists, skipping`, 'alert') } else { api.log(` - creating symbolic link '${destination}' => '${source}'`) fs.symlinkSync(source, destination, 'dir') } } /** * Prepares acton params for logging. * Hides any sensitieve data as defined by `api.config.general.filteredParams` * Truncates long strings via `api.config.logger.maxLogStringLength` * * @param {Object} actionParams Params to filter. * @return {Object} Filtered Params. */ api.utils.filterObjectForLogging = function (actionParams) { let filteredParams = {} for (let i in actionParams) { if (api.utils.isPlainObject(actionParams[i])) { filteredParams[i] = api.utils.objClone(actionParams[i]) } else if (typeof actionParams[i] === 'string') { filteredParams[i] = actionParams[i].substring(0, api.config.logger.maxLogStringLength) } else { filteredParams[i] = actionParams[i] } } api.config.general.filteredParams.forEach((configParam) => { if (api.utils.dotProp.get(actionParams, configParam) !== undefined) { api.utils.dotProp.set(filteredParams, configParam, '[FILTERED]') } }) return filteredParams } next() } }