'use strict'; var async = require('async'); var path = require('path'); var semver = require('semver'); var readline = require('readline'); var winston = require('winston'); var db = require('./database'); var file = require('../src/file'); /* * Need to write an upgrade script for NodeBB? Cool. * * 1. Copy TEMPLATE to a file name of your choice. Try to be succinct. * 2. Open up that file and change the user-friendly name (can be longer/more descriptive than the file name) * and timestamp (don't forget the timestamp!) * 3. Add your script under the "method" property */ var Upgrade = {}; Upgrade.getAll = function (callback) { async.waterfall([ async.apply(file.walk, path.join(__dirname, './upgrades')), function (files, next) { // Sort the upgrade scripts based on version var versionA; var versionB; setImmediate(next, null, files.filter(function (file) { return path.basename(file) !== 'TEMPLATE'; }).sort(function (a, b) { versionA = path.dirname(a).split('/').pop(); versionB = path.dirname(b).split('/').pop(); return semver.compare(versionA, versionB); })); }, async.apply(Upgrade.appendPluginScripts), ], callback); }; Upgrade.appendPluginScripts = function (files, callback) { async.waterfall([ // Find all active plugins async.apply(db.getSortedSetRange.bind(db), 'plugins:active', 0, -1), // Read plugin.json and check for upgrade scripts function (plugins, next) { async.each(plugins, function (plugin, next) { var configPath = path.join(__dirname, '../node_modules', plugin, 'plugin.json'); try { var pluginConfig = require(configPath); if (pluginConfig.hasOwnProperty('upgrades') && Array.isArray(pluginConfig.upgrades)) { pluginConfig.upgrades.forEach(function (script) { files.push(path.join(path.dirname(configPath), script)); }); } next(); } catch (e) { winston.warn('[upgrade/appendPluginScripts] Unable to read plugin.json for plugin `' + plugin + '`. Skipping.'); process.nextTick(next); } }, function (err) { // Return list of upgrade scripts for continued processing next(err, files); }); }, ], callback); }; Upgrade.check = function (callback) { // Throw 'schema-out-of-date' if not all upgrade scripts have run async.waterfall([ async.apply(Upgrade.getAll), function (files, next) { db.getSortedSetRange('schemaLog', 0, -1, function (err, executed) { if (err) { return callback(err); } var remainder = files.filter(function (name) { return executed.indexOf(path.basename(name, '.js')) === -1; }); next(remainder.length > 0 ? new Error('schema-out-of-date') : null); }); }, ], callback); }; Upgrade.run = function (callback) { process.stdout.write('\nParsing upgrade scripts... '); var queue = []; var skipped = 0; async.parallel({ // Retrieve list of upgrades that have already been run completed: async.apply(db.getSortedSetRange, 'schemaLog', 0, -1), // ... and those available to be run available: Upgrade.getAll, }, function (err, data) { if (err) { return callback(err); } queue = data.available.reduce(function (memo, cur) { if (data.completed.indexOf(path.basename(cur, '.js')) === -1) { memo.push(cur); } else { skipped += 1; } return memo; }, queue); Upgrade.process(queue, skipped, callback); }); }; Upgrade.runParticular = function (names, callback) { process.stdout.write('\nParsing upgrade scripts... '); async.waterfall([ async.apply(file.walk, path.join(__dirname, './upgrades')), function (files, next) { var upgrades = files.filter(function (file) { return names.indexOf(path.basename(file, '.js')) !== -1; }); Upgrade.process(upgrades, 0, next); }, ], callback); }; Upgrade.process = function (files, skipCount, callback) { process.stdout.write('OK'.green + ' | '.reset + String(files.length).cyan + ' script(s) found'.cyan + (skipCount > 0 ? ', '.cyan + String(skipCount).cyan + ' skipped'.cyan : '') + '\n'.reset); async.waterfall([ function (next) { async.parallel({ schemaDate: async.apply(db.get, 'schemaDate'), schemaLogCount: async.apply(db.sortedSetCard, 'schemaLog'), }, next); }, function (results, next) { async.eachSeries(files, function (file, next) { var scriptExport = require(file); var date = new Date(scriptExport.timestamp); var version = path.dirname(file).split('/').pop(); var progress = { current: 0, total: 0, incr: Upgrade.incrementProgress, script: scriptExport, date: date, }; process.stdout.write(' → '.white + String('[' + [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()].join('/') + '] ').gray + String(scriptExport.name).reset + '...\n'); // For backwards compatibility, cross-reference with schemaDate (if found). If a script's date is older, skip it if ((!results.schemaDate && !results.schemaLogCount) || (scriptExport.timestamp <= results.schemaDate && semver.lt(version, '1.5.0'))) { readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); readline.moveCursor(process.stdout, 0, -1); process.stdout.write(' → '.white + String('[' + [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()].join('/') + '] ').gray + String(scriptExport.name).reset + '... ' + 'skipped\n'.grey); db.sortedSetAdd('schemaLog', Date.now(), path.basename(file, '.js'), next); return; } // Do the upgrade... scriptExport.method.bind({ progress: progress, })(function (err) { if (err) { process.stdout.write('error\n'.red); return next(err); } readline.clearLine(process.stdout, 0); readline.cursorTo(process.stdout, 0); readline.moveCursor(process.stdout, 0, -1); process.stdout.write(' → '.white + String('[' + [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()].join('/') + '] ').gray + String(scriptExport.name).reset + '... ' + 'OK\n'.green); // Record success in schemaLog db.sortedSetAdd('schemaLog', Date.now(), path.basename(file, '.js'), next); }); }, next); }, function (next) { process.stdout.write('Upgrade complete!\n\n'.green); setImmediate(next); }, ], callback); }; Upgrade.incrementProgress = function (value) { this.current += value || 1; // Redraw the progress bar var percentage = 0; var filled = 0; var unfilled = 15; if (this.total) { percentage = Math.floor((this.current / this.total) * 100) + '%'; filled = Math.floor((this.current / this.total) * 15); unfilled = 15 - filled; } readline.cursorTo(process.stdout, 0); process.stdout.write(' [' + (filled ? new Array(filled).join('#') : '') + new Array(unfilled).join(' ') + '] (' + this.current + '/' + (this.total || '??') + ') ' + percentage + ' '); }; module.exports = Upgrade;