You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
6.1 KiB
JavaScript
201 lines
6.1 KiB
JavaScript
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
const util = require('util');
|
|
const semver = require('semver');
|
|
const readline = require('readline');
|
|
const winston = require('winston');
|
|
|
|
const db = require('./database');
|
|
const file = require('./file');
|
|
const { paths } = require('./constants');
|
|
|
|
/*
|
|
* Need to write an upgrade script for NodeBB? Cool.
|
|
*
|
|
* 1. Copy TEMPLATE to a unique 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
|
|
*/
|
|
|
|
const Upgrade = module.exports;
|
|
|
|
Upgrade.getAll = async function () {
|
|
let files = await file.walk(path.join(__dirname, './upgrades'));
|
|
|
|
// Sort the upgrade scripts based on version
|
|
files = files.filter(file => path.basename(file) !== 'TEMPLATE').sort(function (a, b) {
|
|
const versionA = path.dirname(a).split(path.sep).pop();
|
|
const versionB = path.dirname(b).split(path.sep).pop();
|
|
const semverCompare = semver.compare(versionA, versionB);
|
|
if (semverCompare) {
|
|
return semverCompare;
|
|
}
|
|
const timestampA = require(a).timestamp;
|
|
const timestampB = require(b).timestamp;
|
|
return timestampA - timestampB;
|
|
});
|
|
|
|
await Upgrade.appendPluginScripts(files);
|
|
|
|
// check duplicates and error
|
|
const seen = {};
|
|
const dupes = [];
|
|
files.forEach((file) => {
|
|
if (seen[file]) {
|
|
dupes.push(file);
|
|
} else {
|
|
seen[file] = true;
|
|
}
|
|
});
|
|
if (dupes.length) {
|
|
winston.error('Found duplicate upgrade scripts\n' + dupes);
|
|
throw new Error('[[error:duplicate-upgrade-scripts]]');
|
|
}
|
|
|
|
return files;
|
|
};
|
|
|
|
Upgrade.appendPluginScripts = async function (files) {
|
|
// Find all active plugins
|
|
const plugins = await db.getSortedSetRange('plugins:active', 0, -1);
|
|
plugins.forEach((plugin) => {
|
|
const configPath = path.join(paths.nodeModules, plugin, 'plugin.json');
|
|
try {
|
|
const pluginConfig = require(configPath);
|
|
if (pluginConfig.hasOwnProperty('upgrades') && Array.isArray(pluginConfig.upgrades)) {
|
|
pluginConfig.upgrades.forEach(function (script) {
|
|
files.push(path.join(path.dirname(configPath), script));
|
|
});
|
|
}
|
|
} catch (e) {
|
|
winston.warn('[upgrade/appendPluginScripts] Unable to read plugin.json for plugin `' + plugin + '`. Skipping.');
|
|
}
|
|
});
|
|
return files;
|
|
};
|
|
|
|
Upgrade.check = async function () {
|
|
// Throw 'schema-out-of-date' if not all upgrade scripts have run
|
|
const files = await Upgrade.getAll();
|
|
const executed = await db.getSortedSetRange('schemaLog', 0, -1);
|
|
const remainder = files.filter(name => !executed.includes(path.basename(name, '.js')));
|
|
if (remainder.length > 0) {
|
|
throw new Error('schema-out-of-date');
|
|
}
|
|
};
|
|
|
|
Upgrade.run = async function () {
|
|
console.log('\nParsing upgrade scripts... ');
|
|
|
|
const [completed, available] = await Promise.all([
|
|
db.getSortedSetRange('schemaLog', 0, -1),
|
|
Upgrade.getAll(),
|
|
]);
|
|
|
|
let skipped = 0;
|
|
const queue = available.filter(function (cur) {
|
|
const upgradeRan = completed.includes(path.basename(cur, '.js'));
|
|
if (upgradeRan) {
|
|
skipped += 1;
|
|
}
|
|
return !upgradeRan;
|
|
});
|
|
|
|
await Upgrade.process(queue, skipped);
|
|
};
|
|
|
|
Upgrade.runParticular = async function (names) {
|
|
console.log('\nParsing upgrade scripts... ');
|
|
const files = await file.walk(path.join(__dirname, './upgrades'));
|
|
await Upgrade.appendPluginScripts(files);
|
|
const upgrades = files.filter(file => names.includes(path.basename(file, '.js')));
|
|
await Upgrade.process(upgrades, 0);
|
|
};
|
|
|
|
Upgrade.process = async function (files, skipCount) {
|
|
console.log('OK'.green + ' | '.reset + String(files.length).cyan + ' script(s) found'.cyan + (skipCount > 0 ? ', '.cyan + String(skipCount).cyan + ' skipped'.cyan : ''));
|
|
const [schemaDate, schemaLogCount] = await Promise.all([
|
|
db.get('schemaDate'),
|
|
db.sortedSetCard('schemaLog'),
|
|
]);
|
|
|
|
for (const file of files) {
|
|
/* eslint-disable no-await-in-loop */
|
|
const scriptExport = require(file);
|
|
const date = new Date(scriptExport.timestamp);
|
|
const version = path.dirname(file).split('/').pop();
|
|
const progress = {
|
|
current: 0,
|
|
counter: 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 + '...');
|
|
|
|
// For backwards compatibility, cross-reference with schemaDate (if found). If a script's date is older, skip it
|
|
if ((!schemaDate && !schemaLogCount) || (scriptExport.timestamp <= schemaDate && semver.lt(version, '1.5.0'))) {
|
|
process.stdout.write(' skipped\n'.grey);
|
|
|
|
await db.sortedSetAdd('schemaLog', Date.now(), path.basename(file, '.js'));
|
|
// eslint-disable-next-line no-continue
|
|
continue;
|
|
}
|
|
|
|
// Promisify method if necessary
|
|
if (scriptExport.method.constructor && scriptExport.method.constructor.name !== 'AsyncFunction') {
|
|
scriptExport.method = util.promisify(scriptExport.method);
|
|
}
|
|
|
|
// Do the upgrade...
|
|
try {
|
|
await scriptExport.method.bind({
|
|
progress: progress,
|
|
})();
|
|
} catch (err) {
|
|
console.error('Error occurred');
|
|
throw err;
|
|
}
|
|
|
|
process.stdout.write(' OK\n'.green);
|
|
|
|
// Record success in schemaLog
|
|
await db.sortedSetAdd('schemaLog', Date.now(), path.basename(file, '.js'));
|
|
}
|
|
|
|
console.log('Schema update complete!\n'.green);
|
|
};
|
|
|
|
Upgrade.incrementProgress = function (value) {
|
|
// Newline on first invocation
|
|
if (this.current === 0) {
|
|
process.stdout.write('\n');
|
|
}
|
|
|
|
this.current += value || 1;
|
|
this.counter += value || 1;
|
|
const step = (this.total ? Math.floor(this.total / 100) : 100);
|
|
|
|
if (this.counter > step || this.current >= this.total) {
|
|
this.counter -= step;
|
|
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 = Math.max(0, 15 - filled);
|
|
}
|
|
|
|
readline.cursorTo(process.stdout, 0);
|
|
process.stdout.write(' [' + (filled ? new Array(filled).join('#') : '') + new Array(unfilled).join(' ') + '] (' + this.current + '/' + (this.total || '??') + ') ' + percentage + ' ');
|
|
}
|
|
};
|
|
|
|
require('./promisify')(Upgrade);
|