UNPKG

fedtools-utilities

Version:
1,395 lines (1,294 loc) 40.7 kB
/*eslint indent: 0, no-console: 0*/ /** * Utilities methods that can be used by multiple modules. * * @class utilities * @static * @module fedtools-utilities */ var _ = require('lodash'), async = require('async'), path = require('path'), fs = require('fs-extra'), osenv = require('osenv'), util = require('util'), shelljs = require('shelljs'), crypto = require('crypto'), inquirer = require('inquirer'), moment = require('moment'), url = require('url'), log = require('fedtools-logs'), cmd = require('fedtools-commands'), config = require('fedtools-config'), timeTrackerStart, _formatMillisecondsToHuman, consoleMinions = { 'stderr': process.stderr.write, 'stdout': process.stdout.write, 'log': console.log, 'info': console.info, 'error': console.error }, NB_SPACES_FOR_TAB = 2, DEFAULT_TIME_PRECISION = 3, PROMPT_PASSWORD = 'password', PROMPT_CONFIRM = 'confirm', PROMPT_PROMPT = 'prompt', ALIVE_SIGNAL = 0, KILL_SIGNAL = 'SIGINT', CRYPTO_ALGO = 'aes-256-ctr'; // -- P R O P E R T I E S /** * Use this signal to check if a process is running. * * @property ALIVE_SIGNAL * @type String * @static */ exports.ALIVE_SIGNAL = ALIVE_SIGNAL; /** * Use this signal to kill a running process. * * @property KILL_SIGNAL * @type String * @static */ exports.KILL_SIGNAL = KILL_SIGNAL; /** * Use this property in conjunction with promptAndContinue. * * @property PROMPT_PASSWORD * @type String * @static */ exports.PROMPT_PASSWORD = PROMPT_PASSWORD; /** * Use this property in conjunction with promptAndContinue. * * @property PROMPT_CONFIRM * @type String * @static */ exports.PROMPT_CONFIRM = PROMPT_CONFIRM; /** * Use this property in conjunction with promptAndContinue. * * @property PROMPT_PROMPT * @type String * @static */ exports.PROMPT_PROMPT = PROMPT_PROMPT; // -- P R I V A T E M E T H O D S function _getHomeDir() { return process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE; } // -- P U B L I C M E T H O D S /** * Convert milliseconds to human readeable format. * * @method formatMillisecondsToHuman * @param {Number} ms The milliseconds time to format. * @param {Number} precision The number of digit for the ms part, default to 3. * @param {String} separator The separator string, default to ', '. * @return {String} str A well formatted duration (ex: 7h, 2m, 5s, 6.123ms). */ exports.formatMillisecondsToHuman = _formatMillisecondsToHuman = function (ms, precision, separator) { var duration = moment.duration(ms, 'milliseconds'), arrElapse = []; precision = precision || DEFAULT_TIME_PRECISION; separator = separator || ', '; arrElapse = [ duration.get('days') ? duration.get('days') + 'd' : '', duration.get('hours') ? duration.get('hours') + 'h' : '', duration.get('minutes') ? duration.get('minutes') + 'm' : '', duration.get('seconds') ? duration.get('seconds') + 's' : '', duration.get('ms') ? duration.get('ms').toFixed(precision) + 'ms' : '' ]; return _.compact(arrElapse).join(separator); }; /** * Start or stop a timer. * Stopping the timer will also log the result in minutes, seconds and milliseconds. * The default introductory text ('Elapsed time:') can be overridden. * * @method timeTracker * @param {String} type The timeTracker action type ('start' or 'stop') * @param {String} [label] The introductory text to display when the timer stops. * Default to "Elapsed time: " * @param {Boolean} quiet Flag to turn logging on or off. * Default to on. * When off, the method returns an object * with message and duration. * * @example * utilities.timeTracker('start'); * longRunningTask(); * utilities.timeTracker('stop'); */ exports.timeTracker = function (type, label, quiet) { var MILLI = 1000, NANO = 1000000, elapsedTotal, formattedDuration, intro; if (type === 'start') { timeTrackerStart = process.hrtime(); return timeTrackerStart; } if (type === 'stop') { elapsedTotal = process.hrtime(timeTrackerStart)[0] * MILLI + process.hrtime( timeTrackerStart)[1] / NANO; formattedDuration = _formatMillisecondsToHuman(elapsedTotal); intro = label ? label : 'Elapsed time: '; if (_.isBoolean(quiet) && quiet === true) { return { message: intro, duration: formattedDuration }; } else { log.echo(intro + formattedDuration); } // reset the timer timeTrackerStart = process.hrtime(); return timeTrackerStart; } }; /** * Finds the `wf2/src` path of a wria2 git repository. * * @method getWF2srcPath * @async * @param {Object} options * @param {String} options.cwd The path where to run the command. * @param {Boolean} [options.silent=false] If true, do no log the command line. * @param {Boolean} [options.verbose=false] If true, log stderr/stdout. * @param {Function} done Callback to execute when done. It gets 2 arguments: * @param {String} done.error On success, this will be null. * @param {String} done.srcPath The path to wf2/src of the wria2 repository. */ function _getWF2srcPath(options, done) { var srcPath; require('./git-helper').findGitRootPath(options, function (err, res) { if (err || !res.stdout) { done(err); } else { srcPath = path.join(res.stdout, 'wf2', 'src'); if (fs.existsSync(srcPath)) { done(null, srcPath); } else { done(1); } } }); } /** * Resolve a path and replace "~" with $HOME if needed * * @method resolvePath * @param {String} pathString A path that can contain '~'. * @return {String} A resolved path. */ exports.resolvePath = function (pathString) { if (pathString) { return path.resolve(pathString.replace('~', _getHomeDir())); } }; /** * Checks if the current OS is Windows based. * * @method isWindows * @return {Boolean} True on if the OS is Windows. */ function _isWindows() { return (process.platform === 'win32'); } exports.isWindows = _isWindows; /** * Wrap a string given a maximum length, trying not to break words. * * @method wordWrap * @param {String} str The string to wrap * @param {String} [width=75] The maximum length of the desired width. * @return {Array} An array of wrapped strings. */ exports.wordWrap = function (str, width) { var regex; width = width || 75; if (!str) { return str; } regex = '.{1,' + width + '}(\\s|$)|\\S+?(\\s|$)'; return str.match(new RegExp(regex, 'g')); }; /** * Transform a string to setCamelCase. * * @method setCamelCase * @param {String} input The string to convert to camelCase. * @return {String} The input string converted into camelCase format. */ exports.setCamelCase = function (input) { var str = input.toLowerCase().replace(/-(.)/g, function (match, group1) { return group1.toUpperCase(); }); return str.charAt(0).toUpperCase() + str.slice(1); }; /** * Runs `npm install` in the provided folder if `node_modules` doesn't exist. * * @method installLocalNpmPackages * @async * @param {String} srcPath The path where a package.json file exists. * * @param {Function} done Callback to execute when done. It gets 1 argument: * @param {String} done.error On success, this will be null. */ exports.installLocalNpmPackages = function (srcPath, options, done) { var cmdline, yarnConfig = config.getKey(config.FEDTOOLSRCKEYS.yarnvsnpm) || 'yarn', isYarnAvailable = _isAppInstalled({ name: 'yarn' }), force = false, packageJson = path.join(srcPath, 'package.json'), nodeModulesPath = path.join(srcPath, 'node_modules'); // For API compatibility, options may be the callback... if (options && typeof options === 'function' && !done) { done = options; } else if (options && options.force) { force = true; } // checking if there is a package.json file first... if (!fs.existsSync(packageJson)) { return done(null); } // otherwise install npms! if (!fs.existsSync(nodeModulesPath) || force) { if (yarnConfig === 'yarn' && isYarnAvailable) { cmdline = 'yarn'; } else { cmdline = 'npm install'; } cmd.run(cmdline, { status: true, verbose: false, pwd: srcPath }, function (err, stderr) { if (err) { log.error('Unable to install npm packages!'); done(err, stderr); } else { done(null); } }); } else { done(null); } }; /** * Finds and create a temporary folder based on the OS. If /repo exists, * it will be used on Linux/Mac. * * @method getTemporaryDir * @param {String} subDir Optional: an extra sub folder to append to the * provided temporary folder. * @param {String} rootDir Optional: force the root path (instead of /tmp or * /repo or whatever the system is providing) * @return {String} Path to a temporary folder. */ exports.getTemporaryDir = function (subDir, rootDir) { var osTmpDir, tmpDir; if (_.isString(rootDir) && fs.existsSync(rootDir)) { osTmpDir = path.resolve(rootDir); } else { // forcing /repo or /tmp on linux and mac if (process.platform === 'linux' || process.platform === 'darwin') { if (fs.existsSync('/repo')) { osTmpDir = '/repo'; } else { osTmpDir = '/tmp'; } } else { osTmpDir = osenv.tmpdir(); } } tmpDir = path.join(osTmpDir, 'fedtools-tmp'); if (subDir) { tmpDir = path.join(tmpDir, subDir); } fs.ensureDirSync(tmpDir); return tmpDir; }; /** * Finds the HOME directory based on the OS. * * @method getHomeDir * @return {String} Path to the home directory. */ exports.getHomeDir = _getHomeDir; /** * Helper method to update the framework version string in all the relevant files. * It updates `.shifter.json` and uses Maven for the rest. * This method must be run within a WF-RIA2 folder. * It will display the current version and prompt the user to enter a new one. * * __NOTE__: local combo loader * * if the user enters 'build' or 'combo', the `replace-wf2_combopath` key in the * `.shifter.json` file will be updated with `../../../wria/combo?basePath=build&` * * @method wria2bump * @async * * @param {Function} done Callback to execute when done. It gets 1 argument: * @param {String} done.error On success, this will be null. */ exports.wria2bump = function (done) { var shifterCfg, yuiDocCfg, currentVersion; _getWF2srcPath({ cwd: process.cwd() }, function (err, srcPath) { var shifterJsonFile, yuiDocJsonFile, questions; if (!err && srcPath) { shifterJsonFile = path.join(srcPath, '.shifter.json'); yuiDocJsonFile = path.join(srcPath, 'yuidoc.json'); questions = { type: 'input', name: 'version', message: 'Type the new version number you want to set: ', validate: function (val) { if (!val) { return 'Version cannot be empty...'; } return true; } }; if (!fs.existsSync(shifterJsonFile)) { log.error( 'Ooops! It looks like you\'re missing a .shifter.json configuration file!'); return done(-1); } else { shifterCfg = JSON.parse(fs.readFileSync(shifterJsonFile, 'utf8')); currentVersion = shifterCfg['replace-wf2_version']; log.info('The current version is: ', currentVersion); inquirer.prompt(questions).then(function (answers) { var newVersion = answers.version; shifterCfg['replace-wf2_version'] = newVersion; // special combo loader // if version is 'build' or 'combo', replace // ../../../wria/combo?basePath=@WF2_VERSION@/build& // with // ../../../wria/combo?basePath=build& if (newVersion === 'build' || newVersion === 'combo') { shifterCfg['replace-wf2_combopath'] = '../../../wria/combo?basePath=build&'; } else if (shifterCfg['replace-wf2_combopath'] === '../../../wria/combo?basePath=build&') { log.notice('Restoring replace-wf2_combopath...'); shifterCfg['replace-wf2_combopath'] = '../../../wria/combo?basePath=@WF2_VERSION@/build&'; } // also making sure that combo is ON shifterCfg['replace-wf2_combine'] = 'true'; // as well as cache shifterCfg['replace-wf2_cache_modules'] = 'true'; fs.writeFileSync(shifterJsonFile, JSON.stringify(shifterCfg, null, NB_SPACES_FOR_TAB)); // time to update yuidoc.json, but only if the version tag exists if (fs.existsSync(yuiDocJsonFile)) { yuiDocCfg = JSON.parse(fs.readFileSync(yuiDocJsonFile, 'utf8')); if (yuiDocCfg && yuiDocCfg.project && yuiDocCfg.project.version !== '') { yuiDocCfg.project.version = newVersion; fs.writeFileSync(yuiDocJsonFile, JSON.stringify(yuiDocCfg, null, NB_SPACES_FOR_TAB)); } } async.waterfall([ function (callback) { var cmdline = 'mvn versions:set -DnewVersion=' + newVersion + ' -DgenerateBackupPoms=false'; cmd.run(cmdline, { pwd: path.join(srcPath, '..', '..') }, function (err, stderr, stdout) { callback(err, { stderr: stderr, stdout: stdout }); }); } ], function (err, data) { var stdout, stderr; if (data && data.stdout) { stdout = data.stdout; } if (data && data.stderr) { stderr = data.stderr; } if (!err) { log.echo(); log.info('All files have been updated with the new version.'); log.info('Make sure it looks fine, then stage, commit and push!'); log.info('You can commit by typing the following:'); log.echo(); log.echo(' git commit -am "Version bump to ' + newVersion + '. NO TICKET"'); log.echo(); } done(err, stderr, stdout); }); }); } } else { log.error('Is the current folder a wria2 path?'); log.echo(); return done(-1); } }); }; /** * Checks if a program or a list of programs are available. * * @method isAppInstalled * @param {Array|Object} options An object or an array of objects. * @param {String} options.name The name of the program to check. * @param {String} [options.error] An optional error message to display if * the given program cannot be found. * @return {Boolean} True on success (all programs are available). * * @example * utilities.isAppInstalled([{ * name: 'mvn', * error: 'Maven is not installed on this machine' * }, { * name: 'java', * error: 'Java cannot be executed on this machine' * }]); */ function _isAppInstalled(options) { var result = true; if (!_.isObject(options) && !_.isArray(options)) { throw 'Invalid argument type'; } if (options && !_.isArray(options)) { options = [options]; } options.forEach(function (option) { var appExec = shelljs.which(option.name); if (!appExec) { result = false; if (option.error) { log.error(option.error); } } else { log.debug(appExec); } }); return result; }; /** * Helper method to send a POSIX signal to a running process. * * @method sendSignal * @param {String|Number} pid The recipient process id. * @param {String} signal The signal to send. * @return {Boolean} True on success. */ exports.sendSignal = function (pid, signal) { try { return process.kill(pid, signal); } catch (e) { return false; } }; /** * Helper method to send an email. It is hard coded to use the local WF SMTP host. * * @method sendEmail * @async * @param {Object} options * @param {String} options.attachments The path + filename of a local file to attach. * @param {String} options.from The email 'From' field. * @param {String|Array} options.to The email 'To' field (can be a string or an array of string). * @param {String} options.subject The email 'Subject' field. * @param {String} options.htmlBody The email 'Body' field in HTML format. * * @param {Function} done Callback to execute when done. It gets 1 argument: * @param {String} done.error On success, this will be null. */ exports.sendEmail = function (options, done) { // create reusable transport method (opens pool of SMTP connections) var mailOptions, transporter, attachments, nodemailer = require('nodemailer'), htmlToText = require('nodemailer-html-to-text').htmlToText; if (process.env.FEDTOOLS_PASSWORD) { if (process.env.FEDTOOLS_PASSWORD === 'direct') { transporter = nodemailer.createTransport({ port: 1024 }); } else { transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: 'wfportal@gmail.com', pass: process.env.FEDTOOLS_PASSWORD } }); } } else { transporter = nodemailer.createTransport({ host: config.getKey(config.FEDTOOLSRCKEYS.smtpserver) || 'localhost' }); } // plug html to text into transporter transporter.use('compile', htmlToText()); if (options.attachments) { attachments = [{ filename: path.basename(options.attachments), path: options.attachments }]; } // setup e-mail data with unicode symbols mailOptions = { from: options.from || 'Fedtools <arno.versini@wellsfargo.com>', to: options.to, subject: options.subject, html: options.htmlBody, attachments: attachments ? attachments : [] }; // ignore unauthorized certificate errors // (https://github.com/nodemailer/nodemailer/issues/357) process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // send mail with defined transport object transporter.sendMail(mailOptions, function (error) { if (error) { log.red(error); } transporter.close(); // shut down the connection pool, no more messages if (typeof done === 'function') { done(); } }); }; /** * Helper method to move a file. It uses pipes to circumvent the `mv` limits on * temporary folders (like /tmp). * * @method copyThenEraseSync * @param {String} src The source path + file to move. * @param {String} dst The destination path + file. */ exports.copyThenEraseSync = function (src, dst) { var is = fs.createReadStream(src), os = fs.createWriteStream(dst); is.pipe(os); is.on('end', function () { fs.unlinkSync(src); }); }; /** * Retrieve the current version of OSX. * * @method getOSXVersion * @async * * @param {Function} done Callback to execute when done. It gets 2 arguments: * @param {String} done.error On success, this will be null. * @param {String} done.version On success, this will be the OSX version. */ exports.getOSXVersion = function (done) { if (require('os').type() === 'Darwin') { cmd.run('sw_vers -productVersion', { status: false }, function (err, stderr, stdout) { if (!err && stdout) { done(null, stdout.replace(/\n$/, '')); } else { done(err, stderr, stdout); } }); } }; /** * Tries to open a given URL in the default browser. * * @method openInBrowser * @async * * @param {Object} options * @param {String} options.url The URL to open. * @param {Boolean} [options.confirm=false] If true, prompts the user if they want to open the URL. * @param {String} [options.message] The message to display if the user is prompted. * @param {Boolean} [options.server] If false, do not suggest CTRL+C to stop the server. * * @param {Function} done Callback to execute when done. It gets 1 argument: * @param {String} done.error On success, this will be null. */ exports.openInBrowser = function (options, done) { var cmdline, _openURL, destUrl = url.parse(options.url), questions = {}, cb = done || function () { // nothing to declare }, msg = options.message || 'Open URL in your default browser?'; _openURL = function (arg, cb) { cmd.run(arg, { detached: true, verbose: false, status: false }, cb); }; if (!destUrl.host) { // not a url, maybe a local file? // in that case, need to wrap it with quotes options.url = '"' + options.url + '"'; } switch (process.platform) { case 'darwin': cmdline = 'open ' + options.url; break; case 'linux': cmdline = 'xdg-open ' + options.url; break; case 'win32': cmdline = 'start ' + options.url; break; default: cmdline = 'open ' + options.url; } if (cmdline) { if (_.isBoolean(options.confirm) && options.confirm) { questions = { type: 'confirm', name: 'goodtogo', message: msg, default: true }; inquirer.prompt(questions).then(function (answers) { if (answers.goodtogo) { log.echo('Opening...'); _openURL(cmdline, cb); } else { if (_.isBoolean(options.server) && options.server === false) { log.echo('Bye then...'); } else { log.echo('Bye then... (remember, CTRL+C to stop it)'); } return cb(); } }); } else { _openURL(cmdline, cb); } } else { log.warning('Unable to open the URL...'); return cb(); } }; /** * Method to obfuscate a string. This is NOT secure! * The intend is to simply have a string that is not clearly * visible just by reading it. * * @method obfuscate * @param {String} string The string to obfuscate. * @return {String} An obfuscated string. */ exports.obfuscate = function (string) { // TODO: deprection notice in node 6 for new Buffer // https://github.com/nodejs/node/pull/4682 return new Buffer(string).toString('base64'); }; /** * Method to un-obfuscate a string. This is NOT secure! * The intend is to simply have a string that is not clearly * visible just by reading it. * * @method unObfuscate * @param {String} string The string to unObfuscate. * @return {String} A readeable string. */ exports.unObfuscate = function (string) { // TODO: deprection notice in node 6 for new Buffer // https://github.com/nodejs/node/pull/4682 return new Buffer(string, 'base64').toString(); }; exports.unicorn = function () { // var unicornAscii = fs.readFileSync(path.join(__dirname, '..', 'data', 'unicorn.txt'), 'utf8'); // log.echo(); // log.echo(unicornAscii); // log.echo(); return; }; /** * Method to toggle the console.xxx output to a file. * It actually only take into account log, info and error. * * @method toggleConsole * @param [String] file The filename of the file where to write * the redirected logs. With no parameter, * it re-enables the original log methods. */ exports.toggleConsole = function (options) { var logFile; function _redirectToFile() { return function () { var msg = util.format.apply(this, arguments); logFile.write(msg + '\n'); }; } if (_.isObject(options) && options.file) { logFile = fs.createWriteStream(options.file, { flags: 'w' }); console.log = _redirectToFile(); console.info = _redirectToFile(); console.error = _redirectToFile(); if (options.stderr) { process.stderr.write = _redirectToFile(); } if (options.stdout) { process.stdout.write = _redirectToFile(); } } else { // just restoring the original console.log = consoleMinions.log; console.error = consoleMinions.error; console.info = consoleMinions.info; process.stderr.write = consoleMinions.stderr; process.stdout.write = consoleMinions.stdout; } }; /** * Harmless sudo access to prompt for password (if needed). * This is useful to call before actually calling a real * command that requires sudo access: it prevents messing * up the display. * * @method forceAdminAccess * @async * @param {Function} done The callback to be called once * the password has been entered. */ exports.forceAdminAccess = function (verbose, done) { verbose = verbose || false; if (process.platform === 'win32') { return done(); } if (process.getuid() !== 0) { if (verbose) { log.echo(); log.notice('You may have to provide your password for admin access...'); log.echo(); } cmd.run('sudo test 1', { status: false }, done); } }; /** * Method to automatically generate a HISTORY card based * on git commits and tags. * * @method getHistoryCard * @async * @param {Object} options Configuration object. * @param {String} options.cwd The path to run the git log on. * @param {Array} options.ignore Array of strings to ignore from the * commit logs. * @param {Function} done The callback to be called once * the HISTORY cards has been generated. */ exports.getHistoryCard = function (options, done) { var i, len, tag, tagTitle, sep = '~~~', stopSign = '~stop~', format = '', cmdline, ignore, filteredTags, fcts = [], result = [], MR_FILTER = [ 'Merge branch \'.*\' into \'release-2..*\'', 'Merge branch \'.*\' into \'develop\'' ], AUTHOR_INDEX = 1, TITLE_INDEX = 2, BODY_INDEX = 3, SHA_INDEX, TIMESTAMP_INDEX; require('./git-helper').getAllTags({}, function (err, res) { if (!err && res && res.length) { len = res.length; if (options.ignore && options.ignore.length) { ignore = new RegExp(options.ignore.join('|')); } filteredTags = _.filter(res, function (data) { if (data.match('rc') || data.match('sp')) { return false; } else { return true; } }); if (options.timestamp) { format += sep + '%at'; TITLE_INDEX++; AUTHOR_INDEX++; BODY_INDEX++; TIMESTAMP_INDEX = 1; } if (options.sha) { format += sep + '%H'; TITLE_INDEX++; AUTHOR_INDEX++; BODY_INDEX++; SHA_INDEX = (TIMESTAMP_INDEX === 1) ? 1 + 1 : 1; } format += sep + '%an' + sep + '%s' + sep + '%b'; len = filteredTags.length; for (i = 0; i < len; i++) { if (filteredTags[i] && filteredTags[i + 1]) { // git log filteredTags[0].tag...filteredTags[1].tag --pretty=format:"(%an): %s" some/path // --pretty=format:"%an~~~%s~~~%b~stop~" cmdline = 'git log ' + filteredTags[i] + '...' + filteredTags[i + 1] + ' --pretty=format:"' + format + stopSign + '" ' + options.cwd; tag = filteredTags[i]; tagTitle = (i === 0) ? 'HEAD' : tag; (function (c, t, tt) { fcts.push(function (callback) { cmd.run(c, { status: false }, function (err, stderr, stdout) { if (!err) { stdout = _.map(stdout.split(stopSign), function (item) { var buff = item.split(sep); if (!buff) { return; } // we only want to see MR commits if (options.merge) { if (buff[TITLE_INDEX] && buff[TITLE_INDEX].match(MR_FILTER.join('|'))) { return { author: buff[AUTHOR_INDEX], title: (buff[TITLE_INDEX]) ? buff[TITLE_INDEX].replace('\n', '') : '', body: (buff[BODY_INDEX]) ? buff[BODY_INDEX] : '', timestamp: TIMESTAMP_INDEX ? buff[TIMESTAMP_INDEX] : '', sha: SHA_INDEX ? buff[SHA_INDEX] : '' }; } } else { // we want to ignore some entries if ((ignore && buff[TITLE_INDEX] && buff[TITLE_INDEX].match(ignore)) || (!buff[AUTHOR_INDEX]) || (!buff[TITLE_INDEX])) { return; } // and if we're here, no more filtering, just return the values return { author: buff[AUTHOR_INDEX], title: (buff[TITLE_INDEX]) ? buff[TITLE_INDEX].replace('\n', '') : '', body: (buff[BODY_INDEX]) ? buff[BODY_INDEX] : '', timestamp: TIMESTAMP_INDEX ? buff[TIMESTAMP_INDEX] : '', sha: SHA_INDEX ? buff[SHA_INDEX] : '' }; } }); result.push({ tag: t, tagTitle: tt, logs: _.compact(stdout) }); } callback(null); }); }); })(cmdline, tag, tagTitle); } } async.waterfall(fcts, function (err) { done(err, result); }); } else { return done(err); } }); }; /** * Method that checks if the current folder is a framework component * * @method isComponentPath * @async * @param {String} folder The folder to check. * @param {Function} done Callback to be called once the verification * is done. Arguments will not be null if the * folder is not a component. */ exports.isComponentPath = function (folder, done) { var evidences = [ path.join(folder, 'build.json'), path.join(folder, 'meta'), path.join(folder, '..', 'wf2') ]; async.waterfall([ function (callback) { fs.exists(evidences[0], function (err) { if (err === true) { err = null; } callback(err); }); }, function (callback) { fs.exists(evidences[1], function (err) { if (err === true) { err = null; } callback(err); }); }, function (callback) { fs.exists(evidences[2], function (err) { if (err === true) { err = null; } callback(err); }); } ], function (err) { done(err); }); }; /** * Method that encrypt or decrypt a given file. * * @method cryptographer * @async * @param {Object} options Configuration object. * @param {String} options.file The file to encrypt/decrypt. * @param {String} options.output The output file - if not provided, writes to stdout. * @param {Boolean} options.status Flag to show command status or not. * @param {Boolean} options.encrypt True to encrypt, false to decrypt. * @param {String} options.password The password used to encrypt/decrypt the file. * If not provided, it will be prompted on the CLI. * @param {Function} done Callback to be called once the process is done. Argument * will be null if the encryption/decryption process is successful. */ exports.cryptographer = function (options, done) { var file, promptMsg; function _getPassword(password, msg, cb) { var questions = { type: 'password', name: 'password', message: msg || 'Enter password:', validate: function (val) { if (!val) { return 'Password cannot be empty...'; } return true; } }; if (!password) { inquirer.prompt(questions).then(function (answers) { cb(null, answers.password); }); } else { return cb(null, password); } } function _encrypt(password, buffer) { var cipher = crypto.createCipher(CRYPTO_ALGO, password), crypted = Buffer.concat([cipher.update(buffer), cipher.final()]); return crypted; } function _decrypt(password, buffer) { var decipher = crypto.createDecipher(CRYPTO_ALGO, password), dec = Buffer.concat([decipher.update(buffer), decipher.final()]); return dec; } if (!options.file || !fs.existsSync(options.file)) { throw 'Invalid argument, file is missing'; } else { file = path.resolve(options.file); } if (!options.output) { // no output, let's use stdout and disable status logging. options.status = false; } if (options.encrypt) { promptMsg = 'Enter password to encrypt file:'; } else { promptMsg = 'Enter password to decrypt file:'; } _getPassword(options.password, promptMsg, function (err, pass) { if (err) { throw err; } if (options.encrypt) { if (options.status) { log.info('Encrypting file...'); } fs.readFile(file, function (err, data) { if (err) { throw err; } if (!options.output) { process.stdout.write(_encrypt(pass, data)); return done(null, options); } else { fs.writeFile(options.output, _encrypt(pass, data), function (err) { if (err) { throw err; } if (options.status) { log.success(path.basename(file) + ' was successfully encrypted.'); log.echo('Encrypted file is ', options.output); } return done(null, options); }); } }); } else { if (options.status) { log.info('Decrypting file...'); } fs.readFile(file, function (err, data) { if (err) { throw err; } if (!options.output) { process.stdout.write(_decrypt(pass, data)); return done(null, options); } else { fs.writeFile(options.output, _decrypt(pass, data), function (err) { if (err) { throw err; } if (options.status) { log.success(path.basename(file) + ' was successfully decrypted.'); log.echo('Decrypted file is ', options.output); } return done(null, options); }); } }); } }); }; /** * Method that removes lines from a file. * * @async * @param {Object} options Configuration object. * @param {String} options.file The file to remove the lines from. It has to exist. * @param {String|Array} options.lines The lines to remove (a single string or an array of strings). * @param {Boolean} options.write Default to true. Updates the file accordingly if true. * @param {Function} done Callback to be called once the process is done. 1st argument * will be null if the process is successful. 2nd argument is a string * representation of the updated file (with the removed lines). */ function _removeLinesInFile(options, done) { var buffer, found, res, newBuffer = [], write = (_.isBoolean(options.write)) ? options.write : true; if (!fs.existsSync(options.file)) { return done(null, ''); } if (!_.isArray(options.lines)) { options.lines = [options.lines]; } buffer = fs.readFileSync(options.file, 'utf8').split('\n'); _.each(buffer, function (line) { found = false; _.each(options.lines, function (val) { if (val === line) { found = true; } }); if (!found) { // pattern not found, the line should not be removed newBuffer.push(line); } }); res = newBuffer.join('\n'); if (write) { fs.writeFileSync(options.file, res); } done(null, res); } /** * Method that appends lines to a file. * If the lines already exist in the file, they will be moved at * the bottom of the file. * * @async * @method appendLinesInFile * @param {Object} options Configuration object. * @param {String} options.file The file to remove the lines from. It has to exist. * @param {String|Array} options.lines The lines to append (a single string or an array of strings). * @param {Boolean} options.write Default to true. Updates the file accordingly if true. * @param {Boolean} options.endWithNewLine Add extra new line if true. * @param {appendLinesInFileCB} cb - The callback that handles the response. */ /** * @callback appendLinesInFileCB * @param {String} err Null if the process is successful. * @param {String} buffer String representation of the file with new lines. */ exports.appendLinesInFile = function (options, done) { var write = (_.isBoolean(options.write)) ? options.write : true; if (!_.isArray(options.lines)) { options.lines = [options.lines]; } // before adding files, let's make sure they are not in // the file already, and if they are, remove them... _removeLinesInFile({ lines: options.lines, file: options.file, write: false }, function (err, body) { if (err) { throw err; } if (body) { body += '\n' + options.lines.join('\n'); } else { body = options.lines.join('\n'); } if (options.endWithNewLine) { body += '\n'; } if (write) { fs.writeFileSync(options.file, body); } done(null, body); }); }; /** * Checks if a folder is writable by the calling process. * * @async * @method isFolderWritable * @param {String|Array} dirnames The folder path(s) to check (has to be full path). * @param {Function} done Callback to be called once the process is done. 1st argument * will be null if the folder(s) is (are) writable. */ exports.isFolderWritable = function (dirnames, done) { var writableFlag = fs.W_OK || fs.constants.W_OK, dirs = dirnames; if (_.isString(dirnames)) { dirs = [dirnames]; } if (_.isArray(dirs)) { async.each(dirs, function (item, callback) { fs.access(item, writableFlag, function (err) { if (err && err.code === 'EACCES') { return callback(err); } if (_isWindows()) { fs.stat(item, function (err) { if (err && err.code === 'EPERM') { return callback(err); } return callback(null); }); } else { return callback(null); } }); }, function (err) { done(err); }); } else { return done(1); } }; /** * Finds the location where node modules are globaly installed. * * @method getGlobalNodeModulesPath * @return {String} loc The location of global node modules. */ exports.getGlobalNodeModulesPath = function () { var res = cmd.run('npm root -g', { status: false }); if (res.code === 0 && res.stdout) { return res.stdout.replace(/\n$/, ''); } else { return ''; } }; exports.getHelp = function (debug, options) { var i = 0, MAX_OPTIONS = 10, namespace, _options = []; options.i18n.loadPhrases(path.resolve(__dirname, '..', 'data', 'i18n', 'utilities')); if (options.type === 'bump' || options.type === 'simpleserver') { namespace = 'utilities.help.' + options.type; } if (namespace) { for (i = 0; i < MAX_OPTIONS; i += 1) { _options.push({ option: options.i18n.t(namespace + '.options.' + i + '.option'), desc: options.i18n.t(namespace + '.options.' + i + '.desc') }); } return { namespace: namespace, synopsis: options.i18n.t(namespace + '.synopsis'), command: options.i18n.t(namespace + '.command'), description: options.i18n.t(namespace + '.description'), options: _options, examples: options.i18n.t(namespace + '.examples') }; } return {}; }; exports.removeLinesInFile = _removeLinesInFile; exports.getWF2srcPath = _getWF2srcPath; exports.isAppInstalled = _isAppInstalled; exports.git = require('./git-helper'); exports.simpleserver = require('./simple-http-server/server'); exports.smtpserver = require('./simple-smtp-server/smtp').smtpserver; exports.progress = require('./third/progress'); exports.targz = require('./third/targz'); exports.performance = require('./third/performance'); exports.readdir = require('./third/readdir');