From dff947b32524ebb29d8e541df1d38b554d2826a6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 9 Mar 2016 13:29:37 -0500 Subject: [PATCH] Plugin updates via command line (closes #4336) Squashed commit of the following: commit 9e0d6438dadf645d0d82210631731786f35daf25 Author: Julian Lam Date: Wed Mar 9 13:29:27 2016 -0500 completed integration for plugin upgrade functionality via command line commit 5a27a64a24bdf640bb5c7aaa47cc1d4932ce791c Author: Julian Lam Date: Mon Mar 7 14:47:57 2016 -0500 WIP plugin upgrades for nodebb executable commit 00d5303e339b1077b0ebc5ab3a4aea0e80dc98e9 Author: barisusakli Date: Wed Mar 9 18:06:05 2016 +0200 fix typo --- nodebb | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 192 insertions(+), 9 deletions(-) diff --git a/nodebb b/nodebb index 233dbd350f..d162668344 100755 --- a/nodebb +++ b/nodebb @@ -4,6 +4,10 @@ var colors = require('colors'), cproc = require('child_process'), argv = require('minimist')(process.argv.slice(2)), fs = require('fs'), + path = require('path'), + request = require('request'), + semver = require('semver'), + prompt = require('prompt'), async = require('async'); var getRunningPid = function(callback) { @@ -21,14 +25,185 @@ var getRunningPid = function(callback) { callback(e); } }); - }; + }, + getCurrentVersion = function(callback) { + fs.readFile(path.join(__dirname, 'package.json'), { encoding: 'utf-8' }, function(err, pkg) { + try { + pkg = JSON.parse(pkg); + return callback(null, pkg.version); + } catch(err) { + return callback(err); + } + }) + }, + fork = function (args) { + cproc.fork('app.js', args, { + cwd: __dirname, + silent: false + }); + }, + getInstalledPlugins = function(callback) { + async.parallel({ + files: async.apply(fs.readdir, path.join(__dirname, 'node_modules')), + deps: async.apply(fs.readFile, path.join(__dirname, 'package.json'), { encoding: 'utf-8' }) + }, function(err, payload) { + try { + var isNbbModule = /^nodebb-(?:plugin|theme|widget|rewards)-[\w\-]+$/, + moduleName; -function fork(args) { - cproc.fork('app.js', args, { - cwd: __dirname, - silent: false - }); -} + payload.files = payload.files.filter(function(file) { + return isNbbModule.test(file); + }); + + payload.deps = JSON.parse(payload.deps).dependencies; + payload.bundled = []; + payload.installed = []; + + for (moduleName in payload.deps) { + if (isNbbModule.test(moduleName)) { + payload.bundled.push(moduleName); + } + } + + // Whittle down deps to send back only extraneously installed plugins/themes/etc + payload.files.forEach(function(moduleName) { + if ( + payload.files.indexOf(moduleName) !== -1 // found in `node_modules/` + && payload.bundled.indexOf(moduleName) === -1 // not found in `package.json` + && !fs.lstatSync(path.join(__dirname, 'node_modules/' + moduleName)).isSymbolicLink() // is not a symlink + ) { + payload.installed.push(moduleName); + } + }); + + getModuleVersions(payload.installed, callback); + } catch (err) { + callback(err); + } + }); + }, + getModuleVersions = function(modules, callback) { + var versionHash = {}; + + async.eachLimit(modules, 50, function(module, next) { + fs.readFile(path.join(__dirname, 'node_modules/' + module + '/package.json'), { encoding: 'utf-8' }, function(err, pkg) { + try { + pkg = JSON.parse(pkg); + versionHash[module] = pkg.version; + next(); + } catch (err) { + next(err); + } + }); + }, function(err) { + callback(err, versionHash); + }); + }, + checkPlugins = function(standalone, callback) { + if (standalone) { + process.stdout.write('Checking installed plugins and themes for updates... '); + } + + async.waterfall([ + async.apply(async.parallel, { + plugins: async.apply(getInstalledPlugins), + version: async.apply(getCurrentVersion) + }), + function(payload, next) { + var toCheck = Object.keys(payload.plugins); + + request({ + method: 'GET', + url: 'https://packages.nodebb.org/api/v1/suggest?version=' + payload.version + '&package[]=' + toCheck.join('&package[]='), + json: true + }, function(err, res, body) { + if (err) { + process.stdout.write('error'.red + '\n'.reset); + return next(err); + } + process.stdout.write('OK'.green + '\n'.reset); + + if (!Array.isArray(body) && toCheck.length === 1) { + body = [body]; + } + + var current, suggested, + upgradable = body.map(function(suggestObj) { + current = payload.plugins[suggestObj.package]; + suggested = suggestObj.version; + + if (suggestObj.code === 'match-found' && semver.gt(suggested, current)) { + return { + name: suggestObj.package, + current: current, + suggested: suggested + } + } else { + return null; + } + }).filter(Boolean); + + next(null, upgradable); + }) + } + ], callback); + }, + upgradePlugins = function(callback) { + var standalone = false; + if (typeof callback !== 'function') { + callback = function() {}; + standalone = true; + }; + + checkPlugins(standalone, function(err, found) { + if (err) { + process.stdout.write('\Warning'.yellow + ': An unexpected error occured when attempting to verify plugin upgradability\n'.reset); + return callback(err); + } + + if (found && found.length) { + process.stdout.write('\nA total of ' + new String(found.length).bold + ' package(s) can be upgraded:\n'); + found.forEach(function(suggestObj) { + process.stdout.write(' * '.yellow + suggestObj.name.reset + ' (' + suggestObj.current.yellow + ' -> '.reset + suggestObj.suggested.green + ')\n'.reset); + }); + process.stdout.write('\n'); + } else { + if (standalone) { + process.stdout.write('\nAll packages up-to-date!'.green + '\n'.reset); + } + return callback(); + } + + prompt.message = ''; + prompt.delimiter = ''; + + prompt.start(); + prompt.get({ + name: 'upgrade', + description: 'Proceed with upgrade (y|n)?'.reset, + type: 'string' + }, function(err, result) { + if (result.upgrade === 'y' || result.upgrade === 'yes') { + process.stdout.write('\nUpgrading packages...'); + var args = ['npm', 'i']; + found.forEach(function(suggestObj) { + args.push(suggestObj.name + '@' + suggestObj.suggested); + }); + + require('child_process').execFile('/usr/bin/env', args, { stdio: 'ignore' }, function(err) { + if (!err) { + process.stdout.write(' OK\n'.green); + } + + callback(err); + }); + } else { + process.stdout.write('\nPackage upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade-plugins'.green + '".\n'.reset); + callback(); + } + }) + }); + }; switch(process.argv[2]) { case 'status': @@ -130,15 +305,23 @@ switch(process.argv[2]) { fork(args); break; + case 'upgrade-plugins': + upgradePlugins(); + break; + case 'upgrade': async.series([ function(next) { process.stdout.write('1. '.bold + 'Bringing base dependencies up to date... '.yellow); - require('child_process').execFile('/usr/bin/env', ['npm', 'i', '--production'], next); + require('child_process').execFile('/usr/bin/env', ['npm', 'i', '--production'], { stdio: 'ignore' }, next); }, function(next) { process.stdout.write('OK\n'.green); - process.stdout.write('2. '.bold + 'Updating NodeBB data store schema.\n'.yellow); + process.stdout.write('2. '.bold + 'Checking installed plugins for updates... '.yellow); + upgradePlugins(next); + }, + function(next) { + process.stdout.write('3. '.bold + 'Updating NodeBB data store schema...\n'.yellow); var upgradeProc = cproc.fork('app.js', ['--upgrade'], { cwd: __dirname, silent: false