From 535e544071caab38bfe940d667860e84eb42348c Mon Sep 17 00:00:00 2001 From: Tom Taylor Date: Tue, 17 Oct 2017 13:43:32 +0100 Subject: [PATCH] Enchancements to install/update (#1726) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * can upgrades to upgrade and install script * removed version.json * fix issues caused by removing version.json fix pacakge.json version number. fix issue that stopped automatic upgrade added error catching to gitub api calls * Merge lib/application.js changes from canstudios/latest-release * Add refactored file * Refactor common code into helper file * Move framework & authoring git code into helper * Switch to use helper function Also reduce other duplication * Fix issue with framework folder path * Switch comparison to use semver * Refactor, and switch to use configuration module * Add spinner * Add delegate function to check for update data * Adjust spinner code * Make log a bit more user-friendly * Amend code style * Remove all dependencies on version.json * Remove whole framework folder before clone * Fix import errors * Remove comments * Reword message * Refactor * Start install refactor * Amend imports * Update style * Refactor * Fix broken references * Add newlines * Add temporary callback * Move config items to point of ref, as need to make use of dynamic vars * Set configResults * Refactor tenant code * Add bracket for readability * Remove line * Update tenantmanager to use local framework * Add missing import * Fix to framework install * Keep config.frameworkRevision for now * Add logs * Add error checks * Add custom directory * Fix application update check * Add missing import * Fix error handling * Update functions to use opts values * Disable logging for now * Remove framework install This is now done by tenantmanager when setting up master tenant * Add frontend build function to helpers * Update logs * Amend colouring * Hide exec logs * Add error handling * Move spinner code to helpers * Move exit code to helpers * Abstract input code * Refactor functions for more general use * Add missing bits * Update logs for readability * Fix logic * Remove extraneous code * Add helpful error for GitHub API limit error Amend some other logs for readability * Add missing error var * Update log messages * Amend to use log wrapper * Add spinner to install * Fix issue with halted install process * Fix upgrade version check issue * Allow framework version to be ‘locked’ in config.json Requires ‘framework’ attribute, can be 1, 2, or 3 figures (e.g. ‘1’, ‘1.1’ or ‘1.1.1’) * Allow undefined frameworkRevision value * Add dependency * Set env var to silence install output * Remove adapt_framework folder from .gitignore * Amend in-line with review by @tomgreenfield * Remove unused functions * Refactor * Fix error handling * Fix typos * Add config.json install overrides Also refactor input configs * Switch to use writeJSON * Add input validators/transforms * Refactor for readability * Fix error bug * Fix error check * Remove .env file generation as we don’t support foreman * Update prompt dev to allow password replace * Remove .env and .vagrant * Fix config output * Update feedback texts * Remove reference to version.json * Fix issue with framework install directory * Removed unused lines * Move into single applyEachSeries * Don’t modify existing object * Pass opts to bowermanager * Fix issue with framework plugin isntall * Refactor code * Change prompt.delimiter to match install script * Fix issue with undefined masterTenantID * Make SMTP input optional * Remove redundant code * Tidy output logs * Move init * Stop newline after passwords * Move installHelpers * Move further up * Uncouple function from framework lock * Move input code to installHelpers * Update instructional text * Refactor error handling code * Fix issue with authoring update erroring on git fetch * Move log * Rewrite for readability * Make revision input optional But must specify one… * Refactor for error handling * Fix framework update Now updateRepos always does a fetch Resets to remote/branch * Fix updateRepo to allow any valid revision * Remove logs * Remove node_modules before installing deps * Amend user instruction text * Fix typo * Use backticks * Amend logging * Update log * Fix issue with returning error after exec * Default framework URL to adaptlearning * Return error if trying to upgrade with custom repos * Refactor error handling code * Split up server config items to let us dynamically set rootUrl.default * Remove unused function * Switch processes to use remote name ‘origin’ * Fix config defaults * Return data with callback * Switch spinner library due to issue on windows * Fix broken var reference * Update log for usability * Allow for config.json value * Fix whitespace * Remove log from config.json install * Add db connection check * Move out of async.race due to execution time * Reduce animation framerate * Fix error handler --- .gitignore | 6 - install.js | 958 +++++++++++++++------------------ lib/application.js | 271 +++------- lib/bowermanager.js | 107 ++-- lib/database.js | 24 + lib/frameworkhelper.js | 52 +- lib/installHelpers.js | 531 ++++++++++++++++++ lib/tenantmanager.js | 70 ++- package.json | 26 +- plugins/content/bower/index.js | 91 ++-- plugins/output/adapt/index.js | 14 +- test/entry.js | 2 + upgrade.js | 499 +++++------------ version.json | 4 - 14 files changed, 1382 insertions(+), 1273 deletions(-) create mode 100644 lib/installHelpers.js delete mode 100644 version.json diff --git a/.gitignore b/.gitignore index 20b3452841..8d11a415a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -/adapt_framework - /conf/config.json /data @@ -31,9 +29,5 @@ /.settings -version.json - .idea -.vagrant -.env .project diff --git a/install.js b/install.js index 00086a69b8..281f23a94d 100644 --- a/install.js +++ b/install.js @@ -1,545 +1,479 @@ -// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE -var prompt = require('prompt'), - async = require('async'), - fs = require('fs'), - path = require('path'), - rimraf = require('rimraf'), - exec = require('child_process').exec, - origin = require('./lib/application'), - frameworkHelper = require('./lib/frameworkhelper'), - auth = require('./lib/auth'), - database = require('./lib/database'), - helpers = require('./lib/helpers'), - localAuth = require('./plugins/auth/local'), - logger = require('./lib/logger'), - optimist = require('optimist'), - util = require('util'); +var _ = require('underscore'); +var async = require('async'); +var chalk = require('chalk'); +var fs = require('fs-extra'); +var optimist = require('optimist'); +var path = require('path'); +var prompt = require('prompt'); + +var auth = require('./lib/auth'); +var database = require('./lib/database'); +var helpers = require('./lib/helpers'); +var installHelpers = require('./lib/installHelpers'); +var localAuth = require('./plugins/auth/local'); +var logger = require('./lib/logger'); +var origin = require('./lib/application'); + +var IS_INTERACTIVE = process.argv.length === 2; +var USE_CONFIG; -// set overrides from command line arguments -prompt.override = optimist.argv; -prompt.start(); - -prompt.message = '> '; -prompt.delimiter = ''; - -// get available db drivers and auth plugins -var drivers = database.getAvailableDriversSync(); -var auths = auth.getAvailableAuthPluginsSync(); var app = origin(); +// config for prompt inputs +var inputData; var masterTenant = false; var superUser = false; +// from user input +var configResults; -var isVagrant = function () { - if (process.argv.length > 2) { - return true; +// we need the framework version for the config items, so let's go +installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) { + if(error) { + return handleError(error, 1, 'Failed to get the latest framework version. Check package.json.'); } - - return false; -}; - -// config items -var configItems = [ - { - name: 'serverPort', - type: 'number', - description: 'Server port', - pattern: /^[0-9]+\W*$/, - default: 5000 - }, - { - name: 'serverName', - type: 'string', - description: 'Server name', - default: 'localhost' - }, - // { - // name: 'dbType', - // type: 'string', - // description: getDriversPrompt(), - // conform: function (v) { - // // validate against db drivers - // v = parseInt(v, 10); - // return v > 0 && v <= drivers.length; - // }, - // before: function (v) { - // // convert's the numeric answer to one of the available drivers - // return drivers[(parseInt(v, 10) - 1)]; - // }, - // default: '1' - // }, - { - name: 'dbHost', - type: 'string', - description: 'Database host', - default: 'localhost' - }, - { - name: 'dbName', - type: 'string', - description: 'Master database name', - pattern: /^[A-Za-z0-9_-]+\W*$/, - default: 'adapt-tenant-master' - }, - { - name: 'dbPort', - type: 'number', - description: 'Database server port', - pattern: /^[0-9]+\W*$/, - default: 27017 - }, - { - name: 'dataRoot', - type: 'string', - description: 'Data directory path', - pattern: /^[A-Za-z0-9_-]+\W*$/, - default: 'data' - }, - { - name: 'sessionSecret', - type: 'string', - description: 'Session secret', - pattern: /^.+$/, - default: 'your-session-secret' - }, - // { - // name: 'auth', - // type: 'string', - // description: getAuthPrompt(), - // conform: function (v) { - // // validate against auth types - // v = parseInt(v, 10); - // return v > 0 && v <= auths.length; - // }, - // before: function (v) { - // // convert's the numeric answer to one of the available auth types - // return auths[(parseInt(v, 10) - 1)]; - // }, - // default: '1' - // }, - { - name: 'useffmpeg', - type: 'string', - description: "Will ffmpeg be used? y/N", - before: function (v) { - if (/(Y|y)[es]*/.test(v)) { - return true; + inputData = { + useConfigJSON: { + name: 'useJSON', + description: 'Use existing config values? y/N', + type: 'string', + before: installHelpers.inputHelpers.toBoolean, + default: 'N' + }, + startInstall: { + name: 'install', + description: 'Continue? Y/n', + type: 'string', + before: installHelpers.inputHelpers.toBoolean, + default: 'Y' + }, + server: [ + { + name: 'serverPort', + type: 'number', + description: 'Server port', + pattern: installHelpers.inputHelpers.numberValidator, + default: 5000 + }, + { + name: 'serverName', + type: 'string', + description: 'Server name', + default: 'localhost' + }, + { + name: 'dbHost', + type: 'string', + description: 'Database host', + default: 'localhost' + }, + { + name: 'dbName', + type: 'string', + description: 'Master database name', + pattern: installHelpers.inputHelpers.alphanumValidator, + default: 'adapt-tenant-master' + }, + { + name: 'dbPort', + type: 'number', + description: 'Database server port', + pattern: installHelpers.inputHelpers.numberValidator, + default: 27017 + }, + { + name: 'dataRoot', + type: 'string', + description: 'Data directory path', + pattern: installHelpers.inputHelpers.alphanumValidator, + default: 'data' + }, + { + name: 'sessionSecret', + type: 'string', + description: 'Session secret (value used when saving session cookie data)', + pattern: /^.+$/, + default: 'your-session-secret' + }, + { + name: 'authoringToolRepository', + type: 'string', + description: "Git repository URL to be used for the authoring tool source code", + default: 'https://github.com/adaptlearning/adapt_authoring.git' + }, + { + name: 'frameworkRepository', + type: 'string', + description: "Git repository URL to be used for the framework source code", + default: 'https://github.com/adaptlearning/adapt_framework.git' + }, + { + name: 'frameworkRevision', + type: 'string', + description: 'Specific git revision to be used for the framework. Accepts any valid revision type (e.g. branch/tag/commit)', + default: 'tags/' + latestFrameworkTag + } + ], + features: { + ffmpeg: { + name: 'useffmpeg', + type: 'string', + description: "Are you using ffmpeg? y/N", + before: installHelpers.inputHelpers.toBoolean, + default: 'N' + }, + smtp: { + confirm: { + name: 'useSmtp', + type: 'string', + description: "Will you be using an SMTP server? (used for sending emails) y/N", + before: installHelpers.inputHelpers.toBoolean, + default: 'N' + }, + configure: [ + { + name: 'smtpService', + type: 'string', + description: "Which SMTP service (if any) will be used? (see https://github.com/andris9/nodemailer-wellknown#supported-services for a list of supported services.)", + default: 'none', + }, + { + name: 'smtpUsername', + type: 'string', + description: "SMTP username", + default: '', + }, + { + name: 'smtpPassword', + type: 'string', + description: "SMTP password", + hidden: true, + replace: installHelpers.inputHelpers.passwordReplace, + default: '', + before: installHelpers.inputHelpers.passwordBefore + }, + { + name: 'fromAddress', + type: 'string', + description: "Sender email address", + default: '', + }, + { + name: 'rootUrl', + type: 'string', + description: "The url this install will be accessible from", + default: '' // set using default server options + } + ] } - return false; }, - default: 'N' - }, - { - name: 'smtpService', - type: 'string', - description: "Which SMTP service (if any) will be used? (see https://github.com/andris9/nodemailer-wellknown#supported-services for a list of supported services.)", - default: 'none' - }, - { - name: 'smtpUsername', - type: 'string', - description: "SMTP username", - default: '' - }, - { - name: 'smtpPassword', - type: 'string', - description: "SMTP password", - hidden: true - }, - { - name: 'fromAddress', - type: 'string', - description: "Sender email address", - default: '' - }, - // { - // name: 'outputPlugin', - // type: 'string', - // description: "Which output plugin will be used?", - // default: 'adapt' - // } -]; + tenant: [ + { + name: 'masterTenantName', + type: 'string', + description: "Set a unique name for your tenant", + pattern: installHelpers.inputHelpers.alphanumValidator, + default: 'master' + }, + { + name: 'masterTenantDisplayName', + type: 'string', + description: 'Set the display name for your tenant', + default: 'Master' + } + ], + tenantDelete: { + name: "confirm", + description: "Continue? (Y/n)", + before: installHelpers.inputHelpers.toBoolean, + default: "Y" + }, + superUser: [ + { + name: 'suEmail', + type: 'string', + description: "Email address", + required: true + }, + { + name: 'suPassword', + type: 'string', + description: "Password", + hidden: true, + replace: installHelpers.inputHelpers.passwordReplace, + required: true, + before: installHelpers.inputHelpers.passwordBefore + }, + { + name: 'suRetypePassword', + type: 'string', + description: "Confirm Password", + hidden: true, + replace: installHelpers.inputHelpers.passwordReplace, + required: true, + before: installHelpers.inputHelpers.passwordBefore + } + ] + }; + if(!IS_INTERACTIVE) { + return start(); + } + console.log(''); + if(!fs.existsSync('conf/config.json')) { + return start(); + } + console.log('Found an existing config.json file. Do you want to use the values in this file during install?'); + installHelpers.getInput(inputData.useConfigJSON, function(result) { + console.log(''); + USE_CONFIG = result.useJSON; + start(); + }); +}); -tenantConfig = [ - { - name: 'name', - type: 'string', - description: "Set a unique name for your tenant", - pattern: /^[A-Za-z0-9_-]+\W*$/, - default: 'master' - }, - { - name: 'displayName', - type: 'string', - description: 'Set the display name for your tenant', - required: true, - default: 'Master' +function generatePromptOverrides() { + if(USE_CONFIG) { + var configJson = require('./conf/config.json'); + var configData = JSON.parse(JSON.stringify(configJson).replace('true', '"y"').replace('false', '"n"')); + configData.install = 'y'; } -]; + // NOTE config.json < cmd args + return _.extend({}, configData, optimist.argv); +} -userConfig = [ - { - name: 'email', - type: 'string', - description: "Email address", - required: true - }, - { - name: 'password', - type: 'string', - description: "Password", - hidden: true, - required: true - }, - { - name: 'retypePassword', - type: 'string', - description: "Retype Password", - hidden: true, - required: true +function start() { + // set overrides from command line arguments and config.json + prompt.override = generatePromptOverrides(); + // Prompt the user to begin the install + if(!IS_INTERACTIVE || USE_CONFIG) { + console.log('This script will install the application. Please wait ...'); + } else { + console.log('This script will install the application. \nWould you like to continue?'); } -]; + installHelpers.getInput(inputData.startInstall, function(result) { + if(!result.install) { + return handleError(null, 0, 'User cancelled the install'); + } + async.series([ + configureServer, + configureFeatures, + configureMasterTenant, + createMasterTenant, + createSuperUser, + buildFrontend + ], function(error, results) { + if(error) { + console.error('ERROR: ', error); + return exit(1, 'Install was unsuccessful. Please check the console output.'); + } + exit(0, `Installation completed successfully, the application can now be started with 'node server'.`); + }); + }); +} -/** - * Installer steps - * - * 1. install the framework - * 2. add config vars - * 3. configure master tenant - * 4. create admin account - * 5. TODO install plugins - */ -var steps = [ - // install the framework - function installFramework (next) { - // AB-277 always remove framework folder on install - rimraf(path.resolve(__dirname, 'adapt_framework'), function () { - // now clone the framework - frameworkHelper.cloneFramework(function (err) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Framework install failed. See console output for possible reasons.'); - } +function configureServer(callback) { + console.log(''); + if(!IS_INTERACTIVE || USE_CONFIG) { + console.log('Now setting configuration items.'); + } else { + console.log('We need to configure the tool before install. \nTip: just press ENTER to accept the default value in brackets.'); + } + installHelpers.getLatestFrameworkVersion(function(error, latestFrameworkTag) { + if(error) { + return handleError(error, 1, 'Failed to get latest framework version'); + } + installHelpers.getInput(inputData.server, function(result) { + addConfig(result); + callback(); + }); + }); +} - // Remove the default course - rimraf(path.resolve(__dirname, 'adapt_framework', 'src', 'course'), function(err) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Framework install error -- unable to remove default course.'); +function configureFeatures(callback) { + async.series([ + function ffmpeg(cb) { + installHelpers.getInput(inputData.features.ffmpeg, function(result) { + addConfig(configResults); + cb(); + }); + }, + function smtp(cb) { + installHelpers.getInput(inputData.features.smtp.confirm, function(result) { + if(!result.useSmtp || USE_CONFIG && configResults.useSmtp !== 'y') { + return cb(); + } + for(var i = 0, count = inputData.features.smtp.configure.length; i < count; i++) { + if(inputData.features.smtp.configure[i].name === 'rootUrl') { + inputData.features.smtp.configure[i].default = `http://${configResults.serverName}:${configResults.serverPort}`; } - - return next(); + } + installHelpers.getInput(inputData.features.smtp.configure, function(result) { + addConfig(configResults); + cb(); }); }); - }); - }, - - function configureEnvironment(next) { - if (isVagrant()) { - console.log('Now setting configuration items.'); - } else { - console.log('Now set configuration items. Just press ENTER to accept the default value (in brackets).'); - } - prompt.get(configItems, function (err, results) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Could not save configuration items.'); - } - - saveConfig(results, next); - }); - }, - // configure tenant - function configureTenant (next) { - console.log("Checking configuration, please wait a moment ... "); - // suppress app log output - logger.clear(); + } + ], function() { + saveConfig(configResults, callback); + }); +} - // run the app - app.run(); - app.on('serverStarted', function () { - if (isVagrant()) { - console.log('Creating your tenant. Please wait ...'); - } else { - console.log('Now create your tenant. Just press ENTER to accept the default value (in brackets). Please wait ...'); +function configureMasterTenant(callback) { + var onError = function(error) { + console.error('ERROR: ', error); + return exit(1, 'Failed to configure master tenant. Please check the console output.'); + }; + if(!IS_INTERACTIVE || USE_CONFIG) { + console.log('Now configuring the master tenant. \n'); + } else { + console.log('Now we need to configure the master tenant. \nTip: just press ENTER to accept the default value in brackets.\n'); + } + logger.clear(); + + installHelpers.showSpinner('Starting server'); + // run the app + app.run({ skipVersionCheck: true }); + app.on('serverStarted', function() { + installHelpers.hideSpinner(); + database.checkConnection(function(error) { + if(error) { + return callback(error); } - prompt.get(tenantConfig, function (err, result) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Tenant creation was unsuccessful. Please check the console output.'); - } + installHelpers.getInput(inputData.tenant, function(result) { + console.log(''); + // add the input to our cached config + addConfig({ + masterTenant: { + name: result.masterTenantName, + displayName: result.masterTenantName + } + }); // check if the tenant name already exists - app.tenantmanager.retrieveTenant({ name: result.name }, function (err, tenant) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Tenant creation was unsuccessful. Please check the console output.'); + app.tenantmanager.retrieveTenant({ name: result.masterTenantName }, function(error, tenant) { + if(error) { + return onError(error); } - - var tenantName = result.name; - var tenantDisplayName = result.displayName; - - // create the tenant according to the user provided details - var _createTenant = function (cb) { - console.log("Creating file system for tenant: " + tenantName + ", please wait ..."); - app.tenantmanager.createTenant({ - name: tenantName, - displayName: tenantDisplayName, - isMaster: true, - database: { - dbName: app.configuration.getConfig('dbName'), - dbHost: app.configuration.getConfig('dbHost'), - dbUser: app.configuration.getConfig('dbUser'), - dbPass: app.configuration.getConfig('dbPass'), - dbPort: app.configuration.getConfig('dbPort') - } - }, - function (err, tenant) { - if (err || !tenant) { - console.log('ERROR: ', err); - return exitInstall(1, 'Tenant creation was unsuccessful. Please check the console output.'); - } - - masterTenant = tenant; - console.log("Tenant " + tenant.name + " was created. Now saving configuration, please wait ..."); - // save master tenant name to config - app.configuration.setConfig('masterTenantName', tenant.name); - app.configuration.setConfig('masterTenantID', tenant._id); - saveConfig(app.configuration.getConfig(), cb); - } - ); - }; - - // deletes all collections in the db - var _deleteCollections = function (cb) { - async.eachSeries( - app.db.getModelNames(), - function (modelName, nxt) { - app.db.destroy(modelName, null, nxt); - }, - cb - ); - }; - - if (tenant) { - // deal with duplicate tenant. permanently. - console.log("Tenant already exists. It will be deleted."); - return prompt.get({ name: "confirm", description: "Continue? (Y/n)", default: "Y" }, function (err, result) { - if (err || !/(Y|y)[es]*/.test(result.confirm)) { - return exitInstall(1, 'Exiting install ... '); - } - - // buh-leted - _deleteCollections(function (err) { - if (err) { - return next(err); - } - - return _createTenant(next); - }); - }); + if(!tenant) { + return callback(); } - - // tenant is fresh - return _createTenant(next); + if(!IS_INTERACTIVE) { + return exit(1, `Tenant '${tenant.name}' already exists, automatic install cannot continue.`); + } + console.log(chalk.yellow(`Tenant '${tenant.name}' already exists. ${chalk.underline('It must be deleted for install to continue.')}`)); + installHelpers.getInput(inputData.tenantDelete, function(result) { + console.log(''); + if(!result.confirm) { + return exit(1, 'Exiting install.'); + } + // delete tenant + async.eachSeries(app.db.getModelNames(), function(modelName, cb) { + app.db.destroy(modelName, null, cb); + }, callback); + }); }); }); - }); - }, - // install content plugins - function installContentPlugins (next) { - // Interrogate the adapt.json file from the adapt_framework folder and install the latest versions of the core plugins - fs.readFile(path.join(process.cwd(), 'temp', app.configuration.getConfig('masterTenantID').toString(), 'adapt_framework', 'adapt.json'), function (err, data) { - if (err) { - console.log('ERROR: ' + err); - return next(err); - } - - var json = JSON.parse(data); - // 'dependencies' contains a key-value pair representing the plugin name and the semver - var plugins = Object.keys(json.dependencies); + }, configResults.dbName); + }); +} - async.eachSeries(plugins, function(plugin, pluginCallback) { - if(json.dependencies[plugin] === '*') { - app.bowermanager.installLatestCompatibleVersion(plugin, pluginCallback); - } else { - app.bowermanager.installPlugin(plugin, json.dependencies[plugin], pluginCallback); - } - }, next); - }); - }, - // configure the super awesome user - function createSuperUser (next) { - if (isVagrant()) { - console.log("Creating the super user account. This account can be used to manage everything on your " + app.polyglot.t('app.productname') + " instance."); - } else { - console.log("Create the super user account. This account can be used to manage everything on your " + app.polyglot.t('app.productname') + " instance."); +function createMasterTenant(callback) { + app.tenantmanager.createTenant({ + name: configResults.masterTenant.name, + displayName: configResults.masterTenant.displayName, + isMaster: true, + database: { + dbName: app.configuration.getConfig('dbName'), + dbHost: app.configuration.getConfig('dbHost'), + dbUser: app.configuration.getConfig('dbUser'), + dbPass: app.configuration.getConfig('dbPass'), + dbPort: app.configuration.getConfig('dbPort') } + }, function(error, tenant) { + if(error) { + return handleError(error, 1, 'Failed to create master tenant. Please check the console output.'); + } + console.log('Master tenant created successfully.'); + masterTenant = tenant; + saveConfig(app.configuration.getConfig(), callback); + }); +} - prompt.get(userConfig, function (err, result) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Tenant creation was unsuccessful. Please check the console output.'); - } - - var userEmail = result.email; - var userPassword = result.password; - var userRetypePassword = result.retypePassword; - // ruthlessly remove any existing users (we're already nuclear if we've deleted the existing tenant) - app.usermanager.deleteUser({ email: userEmail }, function (err, userRec) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'User account creation was unsuccessful. Please check the console output.'); +function createSuperUser(callback) { + var onError = function(error) { + handleError(error, 1, 'Failed to create admin user account. Please check the console output.'); + }; + console.log(`\nNow we need to set up a 'Super Admin' account. This account can be used to manage everything on your ${app.polyglot.t('app.productname')} instance.`); + installHelpers.getInput(inputData.superUser, function(result) { + console.log(''); + app.usermanager.deleteUser({ email: result.suEmail }, function(error, userRec) { + if(error) return onError(error); + // add a new user using default auth plugin + new localAuth().internalRegisterUser(true, { + email: result.suEmail, + password: result.suPassword, + retypePassword: result.suRetypePassword, + _tenantId: masterTenant._id + }, function(error, user) { + // TODO should we allow a retry if the passwords don't match? + if(error) { + return onError(error); } - - // add a new user using default auth plugin - new localAuth().internalRegisterUser(true, { - email: userEmail, - password: userPassword, - retypePassword: userRetypePassword, - _tenantId: masterTenant._id - }, function (err, user) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'User account creation was unsuccessful. Please check the console output.'); - } - - superUser = user; - // grant super permissions! - helpers.grantSuperPermissions(user._id, function (err) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'User account creation was unsuccessful. Please check the console output.'); - } - - return next(); - }); - } - ); + superUser = user; + helpers.grantSuperPermissions(user._id, function(error) { + if(error) return onError(error); + return callback(); + }); }); }); - }, - // run grunt build - function gruntBuild (next) { - console.log('Compiling the ' + app.polyglot.t('app.productname') + ' web application, please wait a moment ... '); - var proc = exec('grunt build:prod', { stdio: [0, 'pipe', 'pipe'] }, function (err) { - if (err) { - console.log('ERROR: ', err); - console.log('grunt build:prod command failed. Is the grunt-cli module installed? You can install using ' + 'npm install -g grunt grunt-cli'); - console.log('Install will continue. Try running ' + 'grunt build:prod' + ' after installation completes.'); - return next(); - } - - console.log('The ' + app.polyglot.t('app.productname') + ' web application was compiled and is now ready to use.'); - return next(); - }); - - // pipe through any output from grunt - proc.stdout.on('data', console.log); - proc.stderr.on('data', console.log); - }, - // all done - function finalize (next) { - if (isVagrant()) { - console.log("Installation complete.\nTo restart your instance run the command 'pm2 restart all'"); - } else { - console.log("Installation complete.\n To restart your instance run the command 'node server' (or 'foreman start' if using heroku toolbelt)."); - } - - return next(); - } -]; - -// set overrides from command line arguments -prompt.override = optimist.argv; - -prompt.start(); - -// Prompt the user to begin the install -if (isVagrant()) { - console.log('This script will install the application. Please wait ...'); -} else { - console.log('This script will install the application. Would you like to continue?'); + }); } -prompt.get({ name: 'install', description: 'Y/n', type: 'string', default: 'Y' }, function (err, result) { - if (!/(Y|y)[es]*$/.test(result['install'])) { - return exitInstall(); - } - - // run steps - async.series(steps, function (err, results) { - if (err) { - console.log('ERROR: ', err); - return exitInstall(1, 'Install was unsuccessful. Please check the console output.'); +function buildFrontend(callback) { + installHelpers.buildAuthoring(function(error) { + if(error) { + return callback(`Failed to build the web application, (${error}) \nInstall will continue. Try again after installation completes using 'grunt build:prod'.`); } - - exitInstall(); + callback(); }); -}); +} // helper functions -/** - * This will write out the config items both as a config.json file and - * as a .env file for foreman - * - * @param {object} configItems - * @param {callback} next - */ - -function saveConfig (configItems, next) { - var env = []; - Object.keys(configItems).forEach(function (key) { - env.push(key + "=" + configItems[key]); - }); - - // write the env file! - if (0 === fs.writeSync(fs.openSync('.env', 'w'), env.join("\n"))) { - console.log('ERROR: Failed to write .env file. Do you have write permissions for the current directory?'); - process.exit(1, 'Install Failed.'); - } - - // Defaulting these config settings until there are actual options. - configItems.outputPlugin = 'adapt'; - configItems.dbType = 'mongoose'; - configItems.auth = 'local'; - - // write the config.json file! - if (0 === fs.writeSync(fs.openSync(path.join('conf', 'config.json'), 'w'), JSON.stringify(configItems))) { - console.log('ERROR: Failed to write conf/config.json file. Do you have write permissions for the directory?'); - process.exit(1, 'Install Failed.'); - } - return next(); +function addConfig(newConfigItems) { + configResults = _.extend({}, configResults, newConfigItems); } /** - * writes an indexed prompt for available db drivers + * This will write out the config items both as a config.json file * - * @return {string} + * @param {object} configItems + * @param {callback} callback */ -function getDriversPrompt() { - var str = "Choose your database driver type (enter a number)\n"; - drivers.forEach(function (d, index) { - str += (index+1) + ". " + d + "\n"; +function saveConfig(configItems, callback) { + // add some default values as these aren't set + var config = { + outputPlugin: 'adapt', + dbType: 'mongoose', + auth: 'local', + root: process.cwd() + }; + // copy over the input values + _.each(configItems, function(value, key) { + config[key] = value; }); - - return str; -} - -/** - * writes an indexed prompt for available authentication plugins - * - * @return {string} - */ - -function getAuthPrompt () { - var str = "Choose your authentication method (enter a number)\n"; - auths.forEach(function (a, index) { - str += (index+1) + ". " + a + "\n"; + fs.writeJson(path.join('conf', 'config.json'), config, { spaces: 2 }, function(error) { + if(error) { + handleError(`Failed to write configuration file to ${chalk.underline('conf/config.json')}.\n${error}`, 1, 'Install Failed.'); + } + return callback(); }); +} - return str; +function handleError(error, exitCode, exitMessage) { + if(error) { + console.error(`ERROR: ${error}`); + } + if(exitCode) { + exit(exitCode, exitMessage); + } } /** @@ -549,27 +483,15 @@ function getAuthPrompt () { * @param {string} msg */ -function exitInstall (code, msg) { - code = code || 0; - msg = msg || 'Bye!'; - console.log(msg); - - // handle borked tenant, users, in case of a non-zero exit - if (0 !== code) { - if (app && app.db) { - if (masterTenant) { - return app.db.destroy('tenant', { _id: masterTenant._id }, function (err) { - if (superUser) { - return app.db.destroy('user', { _id: superUser._id }, function (err) { - return process.exit(code); - }); - } - - return process.exit(code); - }); - } +function exit(code, msg) { + installHelpers.exit(code, msg, function(callback) { + if(0 === code || app && !app.db || !masterTenant) { + return callback(); } - } - - process.exit(code); + // handle borked tenant, users, in case of a non-zero exit + app.db.destroy('tenant', { _id: masterTenant._id }, function(error) { + if(!superUser) return callback(); + app.db.destroy('user', { _id: superUser._id }, callback); + }); + }); } diff --git a/lib/application.js b/lib/application.js index ab59adba1a..68c9b1ce4a 100644 --- a/lib/application.js +++ b/lib/application.js @@ -25,9 +25,11 @@ var EventEmitter = require('events').EventEmitter, Mailer = require('./mailer').Mailer, configuration = require('./configuration'); -var request = require('request'); +var installHelpers = require('./installHelpers') + var async = require('async'); var chalk = require('chalk'); +var semver = require('semver'); // Express middleware - separated out in express 4 var favicon = require('serve-favicon'); @@ -279,7 +281,7 @@ Origin.prototype.createServer = function (options, cb) { // new session store using connect-mongo (issue #544) var sessionStore = new MongoStore({ mongooseConnection: db.conn - }); + }); server.use(compression()); /*server.use(favicon());*/ @@ -320,9 +322,6 @@ Origin.prototype.createServer = function (options, cb) { } server.use(permissions.policyChecker()); - - /*server.use(server.router);*/ - // loop through the plugins for middleware var pluginManager = pluginmanager.getManager(); var plugins = pluginManager.getPlugins(); @@ -345,209 +344,78 @@ Origin.prototype.createServer = function (options, cb) { server.use(app.clientErrorHandler()); - - /*if ('development' == server.get('env')) { - server.use(errorHandler({ - dumpExceptions: false, - showStack: false - })); - }*/ - return cb(null, server); }, configuration.getConfig('dbName')); }; Origin.prototype.startServer = function (options) { - + var app = this; // Ensure that the options object is set. - options = typeof options === 'undefined' - ? { skipVersionCheck: false, skipStartLog: false} - : options; - - // configure server - var serverOptions = {}; - if (!this.configuration || !this.configuration.getConfig('dbName')) { - serverOptions.minimal = true; + if(typeof options === 'undefined') { + options = { skipVersionCheck: false, skipStartLog: false }; } - - var app = this; - - var installedServerVersion = '', installedFrameworkVersion = ''; - var latestServerTag = ''; - var latestFrameworkTag = ''; - - async.series([ - function(callback) { - // Read the current versions - var versionFile = JSON.parse(fs.readFileSync('version.json'), {encoding: 'utf8'}); - - if (versionFile) { - installedServerVersion = versionFile.adapt_authoring; - installedFrameworkVersion = versionFile.adapt_framework; + var _checkForUpdates = function(callback) { + if(configuration.getConfig('isTestEnvironment') || options.skipVersionCheck) { + return callback(); + } + checkForUpdates(function(error) { + if(error) { + logger.log('error', `Check for updates failed, ${error}`); } - callback(); - }, - function(callback) { - if (true === configuration.getConfig('isTestEnvironment') || options.skipVersionCheck) { - return callback(); - } - - // Check the latest version of the project - request({ - headers: { - 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36' - }, - uri: 'https://api.github.com/repos/adaptlearning/adapt_authoring/tags', - method: 'GET' - }, function (error, response, body) { - if (error) { - logger.log('error', error); - } else if (response.statusCode == 200) { - var tagInfo = JSON.parse(body); - - if (tagInfo) { - latestServerTag = tagInfo[0].name; - } - } - - callback(); - }); - }, - function(callback) { - if (true === configuration.getConfig('isTestEnvironment') || options.skipVersionCheck) { - return callback(); - } - - // Check the latest version of the framework - request({ - headers: { - 'User-Agent' : 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36' - }, - uri: 'https://api.github.com/repos/adaptlearning/adapt_framework/tags', - method: 'GET' - }, function (error, response, body) { - if (error) { - logger.log('error', error); - } else if (response.statusCode == 200) { - var tagInfo = JSON.parse(body); - - if (tagInfo) { - latestFrameworkTag = tagInfo[0].name; - } - } - - callback(); - }); - }, - function(callback) { - if (true === configuration.getConfig('isTestEnvironment') || options.skipVersionCheck) { - return callback(); - } - - var isUpdateAvailable = false; - - if (installedServerVersion == latestServerTag) { - logger.log('info', chalk.green('%s %s'), app.polyglot.t('app.productname'), installedServerVersion); - } else { - logger.log('info', chalk.yellow('You are currently running %s %s - %s is now available'), app.polyglot.t('app.productname'), installedServerVersion, latestServerTag); - isUpdateAvailable = true; - } - - if (installedFrameworkVersion == latestFrameworkTag) { - logger.log('info', chalk.green('Adapt Framework %s'), installedFrameworkVersion); - } else { - logger.log('info', chalk.yellow('The Adapt Framework being used is %s - %s is now available'), installedFrameworkVersion, latestFrameworkTag); - isUpdateAvailable = true; - } - - if (isUpdateAvailable) { - logger.log('info', "Run " + chalk.bgRed('"node upgrade.js"') + " to update to the latest version"); + }); + }; + _checkForUpdates(function(err, result) { + // configure server + var serverOptions = { + minimal: !app.configuration || !app.configuration.getConfig('dbName') + }; + app.createServer(serverOptions, function (error, server) { + if (error) { + logger.log('fatal', 'error creating server', error); + return process.exit(1); } + // use default port if configuration is not available + var port = app.configuration ? app.configuration.getConfig('serverPort') : DEFAULT_SERVER_PORT; + + app.server = server; + // Create a http server + var httpServer = require('http').createServer(server); + app._httpServer = httpServer.listen(port, function() { + // set up routes + app.router = router(app); + // handle different server states + app.emit(serverOptions.minimal ? "minimalServerStarted" : "serverStarted", app.server); + + var writeRebuildFile = function(courseFolder, callback) { + var OutputConstants = require('./outputmanager').Constants; + var buildFolder = path.join(courseFolder, OutputConstants.Folders.Build); + + fs.exists(buildFolder, function (exists) { + if (!exists) return callback(null); + // Write an empty lock file + logger.log('info', 'Writing build to ' + path.join(buildFolder, OutputConstants.Filenames.Rebuild)); + fs.writeFile(path.join(buildFolder, OutputConstants.Filenames.Rebuild), '', callback); + }); + }; - callback(); - }, - function(callback) { - app.createServer(serverOptions, function (error, server) { - if (error) { - logger.log('fatal', 'error creating server', error); - return process.exit(1); - } - - // use default port if configuration is not available - var port = app.configuration - ? app.configuration.getConfig('serverPort') - : DEFAULT_SERVER_PORT; - - app.server = server; - - // Create a http server - var httpServer = require('http').createServer(server); + app.on("rebuildCourse", function(tenantId, courseId) { + logger.log('info', 'Event:rebuildCourse triggered for Tenant: ' + tenantId + ' Course: ' + courseId); - app._httpServer = httpServer.listen(port, function(){ - // set up routes - app.router = router(app); + var OutputConstants = require('./outputmanager').Constants; + var courseFolder = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), OutputConstants.Folders.Framework, OutputConstants.Folders.AllCourses, tenantId, courseId); - // handle different server states - if (serverOptions.minimal) { - app.emit("minimalServerStarted", app.server); - } else { - app.emit("serverStarted", app.server); - } + fs.exists(courseFolder, function(exists) { + if (!exists) return; - var writeRebuildFile = function(courseFolder, callback) { - var OutputConstants = require('./outputmanager').Constants; - var buildFolder = path.join(courseFolder, OutputConstants.Folders.Build); - - fs.exists(buildFolder, function (exists) { - if (exists) { - // Write an empty lock file called .rebuild - logger.log('info', 'Writing build to ' + path.join(buildFolder, OutputConstants.Filenames.Rebuild)); - fs.writeFile(path.join(buildFolder, OutputConstants.Filenames.Rebuild), '', function (err) { - if (err) { - return callback(err); - } - - return callback(null); - }); - } else { - return callback(null); - } - }); - }; - - app.on("rebuildCourse", function(tenantId, courseId) { - logger.log('info', 'Event:rebuildCourse triggered for Tenant: ' + tenantId + ' Course: ' + courseId); - // Not ideal, but there is a timing issue which prevente d - var OutputConstants = require('./outputmanager').Constants; - var courseFolder = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), OutputConstants.Folders.Framework, OutputConstants.Folders.AllCourses, tenantId, courseId); - - fs.exists(courseFolder, function(exists) { - if (exists) { - writeRebuildFile(courseFolder, function(err) { - if (err) { - logger.log('error', err); - } - - return; - }); - } - - return; + writeRebuildFile(courseFolder, function(err) { + if (err) logger.log('error', err); }); }); - - if (!options.skipStartLog) { - logger.log('info', 'Server started listening on port ' + port); - } - - callback(); }); }); - } - ]); - - + }); + }); }; Origin.prototype.restartServer = function () { @@ -667,6 +535,29 @@ Origin.prototype.ModulePreloader.defOpts = { } }; +function checkForUpdates(callback) { + installHelpers.getInstalledVersions(function(error, installedData) { + installHelpers.getUpdateData(function(error, updateData) { + if(error) { + return callback(error); + } + if (updateData && updateData.adapt_authoring) { + logger.log('info', chalk.yellow(`${app.polyglot.t('app.productname')} v${installedData.adapt_authoring} (${updateData.adapt_authoring} is now available).`)); + } else { + logger.log('info', chalk.green(`${app.polyglot.t('app.productname')} ${installedData.adapt_authoring}.`)); + } + if (updateData && updateData.adapt_framework) { + logger.log('info', chalk.yellow(`Adapt framework v${installedData.adapt_framework} (${updateData.adapt_framework} is now available).`)); + } else { + logger.log('info', chalk.green(`Adapt framework ${installedData.adapt_framework}.`)); + } + if (updateData) { + logger.log('info', `Run ${chalk.bgRed('node upgrade.js')} to update your install.`); + } + callback(); + }); + }); +} /** * boostraps the application or returns it if it exists diff --git a/lib/bowermanager.js b/lib/bowermanager.js index 5f58f7ad87..a96fd1090a 100644 --- a/lib/bowermanager.js +++ b/lib/bowermanager.js @@ -8,6 +8,7 @@ var path = require('path'); var rimraf = require('rimraf'); var semver = require('semver'); +var installHelpers = require('./installHelpers'); var Constants = require('./outputmanager').Constants; var database = require('./database'); var logger = require('./logger'); @@ -71,22 +72,11 @@ BowerManager.prototype.extractPackageInfo = function(plugin, pkgMeta, schema) { */ BowerManager.prototype.installPlugin = function(pluginName, pluginVersion, callback) { var self = this; - // Formulate the package name. var packageName = (pluginVersion == '*') ? pluginName : pluginName + '#' + pluginVersion; - // Interrogate the version.json file on installation as we cannot rely on including it via a 'require' call. - fs.readFile(path.join(app.configuration.getConfig('root'), 'version.json'), 'utf8', function(err, version) { - // Ensure the JSON is parsed. - version = JSON.parse(version); - - if (err) { - logger.log('error', err); - return callback(err); - } - // Clear the bower cache for this plugin. rimraf(path.join(bowerOptions.directory, pluginName), function (err) { if (err) { @@ -135,23 +125,25 @@ BowerManager.prototype.installPlugin = function(pluginName, pluginVersion, callb if (err) { return callback(err); } - - if (packageInfo[pluginName].pkgMeta.framework) { + installHelpers.getLatestFrameworkVersion(function(error, frameworkVersion) { + if (error) { + return callback(error); + } + if (!packageInfo[pluginName].pkgMeta.framework) { + return self.importPackage(plugin, packageInfo[pluginName], bowerOptions, callback); + } // If the plugin defines a framework, ensure that it is compatible - if (semver.satisfies(semver.clean(version.adapt_framework), packageInfo[pluginName].pkgMeta.framework)) { - self.importPackage(plugin, packageInfo[pluginName], bowerOptions, callback); - } else { - logger.log('error', 'Unable to install ' + packageInfo[pluginName].pkgMeta.name + '(' + packageInfo[pluginName].framework + ') as it is not supported in the current version of of the Adapt framework (' + version.adapt_framework + ')'); - return callback('Unable to install ' + packageInfo[pluginName].pkgMeta.name + ' as it is not supported in the current version of of the Adapt framework'); + if (!semver.satisfies(semver.clean(frameworkVersion), packageInfo[pluginName].pkgMeta.framework)) { + var error = `Unable to install ${packageInfo[pluginName].pkgMeta.name} (${packageInfo[pluginName].framework}) as it is not supported in the current version of the Adapt framework (${frameworkVersion})`; + logger.log('error', error); + return callback(error); } - } else { self.importPackage(plugin, packageInfo[pluginName], bowerOptions, callback); - } + }); }); }); }); }); - }); } /** @@ -162,43 +154,44 @@ BowerManager.prototype.installPlugin = function(pluginName, pluginVersion, callb */ BowerManager.prototype.installLatestCompatibleVersion = function (pluginName, callback) { var self = this; - fs.readJson(path.join(app.configuration.getConfig('root'), 'version.json'), function(error, versionJson) { - if (error) { - logger.log('error', error); - return callback(err); + // Query bower to verify that the specified plugin exists. + bower.commands.search(pluginName, bowerOptions) + .on('error', callback) + .on('end', function (results) { + if (!results || results.length == 0) { + logger.log('warn', 'Plugin ' + packageName + ' not found!'); + return callback('Plugin ' + packageName + ' not found!'); } - // Query bower to verify that the specified plugin exists. - bower.commands.search(pluginName, bowerOptions) + // The plugin exists -- remove any fuzzy matches, e.g. adapt-contrib-assessment would + // also bring in adapt-contrib-assessmentResults, etc. + var bowerPackage = _.findWhere(results, {name: pluginName}); + bower.commands.info(bowerPackage.url) .on('error', callback) - .on('end', function (results) { - if (!results || results.length == 0) { - logger.log('warn', 'Plugin ' + packageName + ' not found!'); - return callback('Plugin ' + packageName + ' not found!'); - } - // The plugin exists -- remove any fuzzy matches, e.g. adapt-contrib-assessment would - // also bring in adapt-contrib-assessmentResults, etc. - var bowerPackage = _.findWhere(results, {name: pluginName}); - bower.commands.info(bowerPackage.url) - .on('error', callback) - .on('end', function (latestInfo) { - // the versions will be in version order, rather than release date, - // so no need for ordering - var installedFrameworkVersion = versionJson.adapt_framework; - var requiredFrameworkVersion; - var index = -1; - async.doUntil(function iterator(cb) { - bower.commands.info(bowerPackage.url + '#' + latestInfo.versions[++index]) - .on('error', cb) - .on('end', function (result) { - requiredFrameworkVersion = result.framework; - cb(); - }); - }, function isCompatible() { - return semver.satisfies(installedFrameworkVersion, requiredFrameworkVersion); - }, function(error, version) { - self.installPlugin(pluginName, latestInfo.versions[index], callback); - }); + .on('end', function (latestInfo) { + // the versions will be in version order, rather than release date, + // so no need for ordering + installHelpers.getInstalledFrameworkVersion(function(error, installedFrameworkVersion) { + if(error) { + return callback(error); + } + var requiredFrameworkVersion; + var index = -1; + async.doUntil(function iterator(cb) { + bower.commands.info(bowerPackage.url + '#' + latestInfo.versions[++index]) + .on('error', cb) + .on('end', function (result) { + requiredFrameworkVersion = result.framework; + cb(); + }); + }, function isCompatible() { + return semver.satisfies(installedFrameworkVersion, requiredFrameworkVersion); + }, function(error, version) { + if(error) { + return callback(error); + } + self.installPlugin(pluginName, latestInfo.versions[index], callback); }); + }); }); }); } @@ -257,8 +250,8 @@ BowerManager.prototype.importPackage = function (plugin, packageInfo, options, c return callback(null); } - // Build a path to the destination working folder. - var destination = path.join(app.configuration.getConfig('root').toString(), 'temp', app.configuration.getConfig('masterTenantID').toString(), 'adapt_framework', 'src', plugin.bowerConfig.srcLocation, pkgMeta.name); + // use the passed dest, or build a path to the destination working folder + var destination = path.join(app.configuration.getConfig('root').toString(), 'temp', app.configuration.getConfig('masterTenantID'), 'adapt_framework', 'src', plugin.bowerConfig.srcLocation, pkgMeta.name); // Remove whatever version of the plugin is there already. rimraf(destination, function(err) { diff --git a/lib/database.js b/lib/database.js index 1f26ff407d..f4a8c6c1c1 100644 --- a/lib/database.js +++ b/lib/database.js @@ -491,6 +491,29 @@ function getDatabase(next, tenantId, type) { }); } +/** + * Attempts to connect to the database, and returns an error on failure + * + * @param {callback} cb - function + */ +function checkConnection(cb) { + var name = configuration.getConfig('dbName'); + var host = configuration.getConfig('dbHost'); + var port = configuration.getConfig('dbPort'); + if(!name || !host || !port) { + return cb('Cannot check database connection, missing settings in config.json.'); + } + getDatabase(function(error, db) { + if(error) { + return cb(error); + } + if(db.conn.readyState !== 1) { + return cb(`Cannot connect to the MongoDB '${name}' at '${host}:${port}'. Please check the details are correct and the database is running.`); + } + cb(); + }, name); +} + /** * returns a list of available drivers * @@ -589,6 +612,7 @@ function preloadHandle(app, instance){ */ module.exports.addDatabaseHook = addDatabaseHook; module.exports.getDatabase = getDatabase; +module.exports.checkConnection = checkConnection; module.exports.resolveSchemaPath = resolveSchemaPath; module.exports.getAvailableDrivers = getAvailableDrivers; module.exports.getAvailableDriversSync = getAvailableDriversSync; diff --git a/lib/frameworkhelper.js b/lib/frameworkhelper.js index 1e6d7ad6b3..3482356b78 100644 --- a/lib/frameworkhelper.js +++ b/lib/frameworkhelper.js @@ -1,10 +1,10 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE var path = require('path'), - fs = require('fs'), - util = require('util'), - rimraf = require('rimraf'), - exec = require('child_process').exec, - serverRoot = require('./configuration').serverRoot; + fs = require('fs'), + util = require('util'), + rimraf = require('rimraf'), + exec = require('child_process').exec, + serverRoot = require('./configuration').serverRoot; // errors function FrameworkError (message) { @@ -15,15 +15,13 @@ function FrameworkError (message) { util.inherits(FrameworkError, Error); -var FRAMEWORK_DIR = 'adapt_framework', - DEFAULT_BRANCH = 'master', - GIT_FRAMEWORK_CLONE_URL = 'https://github.com/adaptlearning/adapt_framework.git'; - +var FRAMEWORK_DIR = 'adapt_framework'; + function flog(msg) { console.log(' ' + msg); } -function cloneFramework (next) { +function cloneFramework (next, frameworkRepository, frameworkRevision) { fs.exists (path.join(serverRoot, FRAMEWORK_DIR), function (exists) { if (exists) { // don't bother installing again @@ -32,42 +30,34 @@ function cloneFramework (next) { } console.log('The Adapt Framework was not found. It will now be installed...'); - var child = exec('git clone ' + GIT_FRAMEWORK_CLONE_URL, { + var child = exec('git clone ' + frameworkRepository, { stdio: [0, 'pipe', 'pipe'] }); - + child.stdout.on('data', flog); child.stderr.on('data', flog); - + child.on('exit', function (error, stdout, stderr) { if (error) { console.log('ERROR: ' + error); return next(error); } - + console.log("Clone from GitHub was successful."); - return installFramework(next); + return installFramework(next, frameworkRevision); }); }); }; -function installFramework (next) { - var frameworkVersion = DEFAULT_BRANCH; +function installFramework (next, frameworkRevision) { - try { - var versionFile = JSON.parse(fs.readFileSync('version.json'), {encoding: 'utf8'}); - frameworkVersion = versionFile.adapt_framework; - } catch (e) { - console.log('Warning: Failed to determine compatible Adapt Framework version from version.json, so using ' + DEFAULT_BRANCH); - } - console.log('Running \'npm install\' for the Adapt Framework...'); - - var child = exec('git checkout --quiet ' + frameworkVersion + ' && npm install', { - cwd: FRAMEWORK_DIR, + + var child = exec('git checkout --quiet ' + frameworkRevision + ' && npm install', { + cwd: FRAMEWORK_DIR, stdio: [0, 'pipe', 'pipe'] }); - + child.stdout.on('data', flog); child.stderr.on('data', flog); @@ -76,15 +66,15 @@ function installFramework (next) { console.log('ERROR: ', error); return next(error); } - + console.log("Completed installing NodeJS modules.\n"); - + // Remove the default course. rimraf(path.join(serverRoot, FRAMEWORK_DIR, 'src', 'course'), function(err) { if (err) { console.log('ERROR:', error); } - + return next(); }); }); diff --git a/lib/installHelpers.js b/lib/installHelpers.js new file mode 100644 index 0000000000..2e3a7319e5 --- /dev/null +++ b/lib/installHelpers.js @@ -0,0 +1,531 @@ +var _ = require('underscore'); +var async = require('async'); +var chalk = require('chalk'); +var exec = require('child_process').exec; +var fs = require('fs-extra'); +var logUpdate = require('log-update'); +var path = require('path'); +var prompt = require('prompt'); +var readline = require('readline'); +var request = require('request'); +var semver = require('semver'); + +var configuration = require('./configuration'); +var pkg = fs.readJSONSync(path.join(__dirname, '..', 'package.json')); + +var DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'; +var DEFAULT_GITHUB_ORG = 'adaptlearning'; // used to pull releases from +var DEFAULT_SERVER_REPO = `https://github.com/${DEFAULT_GITHUB_ORG}/adapt_authoring.git`; +var DEFAULT_FRAMEWORK_REPO = `https://github.com/${DEFAULT_GITHUB_ORG}/adapt_framework.git`; +var REMOTE_NAME = 'origin'; + +var spinnerInt = -1; + +var inputHelpers = { + passwordReplace: '*', + numberValidator: /^[0-9]+\W*$/, + alphanumValidator: /^[A-Za-z0-9_-]+\W*$/, + toBoolean: function(v) { + if(/(Y|y)[es]*/.test(v)) return true; + return false; + }, + passwordBefore: function(v) { + /** + * HACK because read module used by prompt adds a blank line when + * hidden & replace attrs are set + */ + readline.moveCursor(process.stdout, 0, -1); + return v; + } +}; + +var exports = module.exports = { + DEFAULT_SERVER_REPO, + DEFAULT_FRAMEWORK_REPO, + exit, + showSpinner, + hideSpinner, + getInput, + inputHelpers, + getInstalledServerVersion, + getLatestServerVersion, + getInstalledFrameworkVersion, + getLatestFrameworkVersion, + getInstalledVersions, + getLatestVersions, + getUpdateData, + installFramework, + updateFramework, + updateFrameworkPlugins, + updateAuthoring, + buildAuthoring +}; + +function exit(code, msg, preCallback) { + var _exit = function() { + hideSpinner(); + code = code || 0; + msg = msg || 'Bye!'; + log('\n' + (code === 0 ? chalk.green(msg) : chalk.red(msg)) + '\n'); + process.exit(code); + } + if(preCallback) { + preCallback(_exit); + } else { + _exit(); + } +} + +function showSpinner(text) { + if(isSilent()) return; + // NOTE we stop the existing spinner (not ideal) + hideSpinner(); + var frames = ['-', '\\', '|', '/']; + var i = 0; + spinnerInt = setInterval(function() { + var frame = frames[i = ++i % frames.length]; + logUpdate(`${frame} ${text}`); + }, 120); +} + +function hideSpinner() { + if(isSilent()) return; + clearInterval(spinnerInt); + logUpdate.clear(); +} + +function getInput(items, callback) { + prompt.message = '> '; + prompt.delimiter = ''; + prompt.start(); + prompt.get(items, function(error, result) { + if(error) { + if(error.message === 'canceled') error = new Error('User cancelled the process'); + return exit(1, error); + } + callback(result); + }); +} + +function getInstalledServerVersion(callback) { + try { + var pkg = fs.readJSONSync('package.json'); + callback(null, pkg.version); + } catch(e) { + callback(`Cannot determine authoring tool version\n${e}`); + } +} + +function getLatestServerVersion(callback) { + checkLatestAdaptRepoVersion('adapt_authoring', callback); +} + +function getInstalledFrameworkVersion(callback) { + try { + var pkg = fs.readJSONSync(path.join(getFrameworkRoot(), 'package.json')); + callback(null, pkg.version); + } catch(e) { + return callback(`Cannot determine framework version\n${e}`); + } +} + +function getLatestFrameworkVersion(callback) { + checkLatestAdaptRepoVersion('adapt_framework', pkg.framework, callback); +} + +function getInstalledVersions(callback) { + async.parallel([ + exports.getInstalledServerVersion, + exports.getInstalledFrameworkVersion + ], function(error, results) { + callback(error, { + adapt_authoring: results[0], + adapt_framework: results[1] + }); + }); +} + +function getLatestVersions(callback) { + async.parallel([ + exports.getLatestServerVersion, + exports.getLatestFrameworkVersion + ], function(error, results) { + callback(error, { + adapt_authoring: results[0], + adapt_framework: results[1] + }); + }); +} + +function getUpdateData(callback) { + async.parallel([ + exports.getInstalledVersions, + exports.getLatestVersions + ], function(error, results) { + if(error) { + return callback(error); + } + var updateData = {}; + if(semver.lt(results[0].adapt_authoring, results[1].adapt_authoring)) { + updateData.adapt_authoring = results[1].adapt_authoring; + } + if(semver.lt(results[0].adapt_framework, results[1].adapt_framework)) { + updateData.adapt_framework = results[1].adapt_framework; + } + if(_.isEmpty(updateData)) { + return callback(); + } + callback(null, updateData); + }); +} + +function getFrameworkRoot() { + return path.join(configuration.serverRoot, 'temp', configuration.getConfig('masterTenantID'), 'adapt_framework'); +} + +/** +* Checks all releases for the latest to match framework value in config.json +* Recusion required for pagination. +*/ +function checkLatestAdaptRepoVersion(repoName, versionLimit, callback) { + if(typeof versionLimit === 'function') { + callback = versionLimit; + versionLimit = undefined; + } + // used in pagination + var nextPage = `https://api.github.com/repos/${DEFAULT_GITHUB_ORG}/${repoName}/releases`; + + var _getReleases = function(done) { + request({ + headers: { + 'User-Agent': DEFAULT_USER_AGENT , + Authorization: 'token 15e160298d59a7a70ac7895c9766b0802735ac99' + }, + uri: nextPage, + method: 'GET' + }, done); + }; + var _requestHandler = function(error, response, body) { + // we've exceeded the API limit + if(response.statusCode === 403 && response.headers['x-ratelimit-remaining'] === '0') { + var reqsReset = new Date(response.headers['x-ratelimit-reset']*1000); + error = `You have exceeded GitHub's request limit of ${response.headers['x-ratelimit-limit']} requests per hour. Please wait until at least ${reqsReset.toTimeString()} before trying again.`; + } + else if (response.statusCode !== 200) { + error = 'GitubAPI did not respond with a 200 status code.'; + } + + if (error) { + return callback(`Couldn't check latest version of ${repoName}\n${error}`); + } + nextPage = parseLinkHeader(response.headers.link).next; + try { + var releases = JSON.parse(body); + } catch(e) { + return callback(`Failed to parse GitHub release data\n${e}`); + } + var compatibleRelease; + if(!versionLimit) { + return callback(null, releases[0].tag_name); + } + async.someSeries(releases, function(release, cb) { + var isFullRelease = !release.draft && !release.prerelease; + if(isFullRelease && semver.satisfies(release.tag_name, versionLimit)) { + compatibleRelease = release; + return cb(null, true); + } + cb(null, false); + }, function(error, satisfied) { + if(!satisfied) { + if(nextPage) { + return _getReleases(_requestHandler); + } + error = `Couldn't find any releases compatible with specified framework version (${versionLimit}), please check that it is a valid version.`; + } + if(error) { + return callback(error); + } + callback(error, compatibleRelease.tag_name); + }); + }; + // start recursion + _getReleases(_requestHandler); +} + +// taken from https://gist.github.com/niallo/3109252 +function parseLinkHeader(header) { + if (!header || header.length === 0) { + return []; + } + var links = {}; + // Parse each part into a named link + _.each(header.split(','), function(p) { + var section = p.split(';'); + if (section.length !== 2) { + throw new Error("section could not be split on ';'"); + } + var url = section[0].replace(/<(.*)>/, '$1').trim(); + var name = section[1].replace(/rel="(.*)"/, '$1').trim(); + links[name] = url; + }); + return links; +} + +/** +* Clones/updates the temp/ framework folder +* Accepts the following options: { +* repository: URL to pull framework from, +* revision: in the format tags/[TAG] or remote/[BRANCH], +* force: forces a clone regardless of whether we have an existing clone, +* } +*/ +function installFramework(opts, callback) { + if(arguments.length !== 2 || !opts.directory) { + return callback('Cannot install framework, invalid options passed.'); + } + if(!opts.repository) { + opts.repository = DEFAULT_FRAMEWORK_REPO; + } + if(!opts.revision) { + return getLatestFrameworkVersion(function(error, version) { + if(error) return callback(error); + installFramework(_.extend({ revision: version }, opts), callback); + }); + } + if(!fs.existsSync(opts.directory) || opts.force) { + return async.applyEachSeries([ + cloneRepo, + updateFramework + ], opts, callback); + } + updateFramework(opts, callback); +} + +function updateFramework(opts, callback) { + if(opts && !opts.repository) { + opts.repository = DEFAULT_FRAMEWORK_REPO; + } + async.applyEachSeries([ + updateRepo, + installDependencies, + purgeCourseFolder, + updateFrameworkPlugins + ], opts, callback); +} + +function checkOptions(opts, action, callback) { + if(!opts) { + return callback(`Cannot ${action} repository, invalid options passed.`); + } + if(!opts.repository) { + return callback(`Cannot ${action} repository, no repository specified.`); + } + if(!opts.directory) { + return callback(`Cannot ${action} ${opts.repository}, no target directory specified.`); + } + callback(); +} + +function cloneRepo(opts, callback) { + checkOptions(opts, 'clone', function(error) { + if(error) { + return callback(error); + } + showSpinner(`Cloning ${opts.repository}`); + fs.remove(opts.directory, function(error) { + if(error) { + hideSpinner(); + return callback(error); + } + execCommand(`git clone ${opts.repository} --origin ${REMOTE_NAME} ${opts.directory}`, function(error) { + hideSpinner(); + if(error) { + return callback(error); + } + log(`Cloned ${opts.repository} successfully.`); + callback(); + }); + }); + }); +} + +function fetchRepo(opts, callback) { + checkOptions(opts, 'fetch', function(error) { + if(error) { + return callback(error); + } + execCommand(`git fetch ${REMOTE_NAME}`, { cwd: opts.directory }, function(error) { + // HACK not an ideal way to figure out if it's the right error... + if(error && error.indexOf(`'${REMOTE_NAME}' does not appear to be a git repository`) > -1) { + error = `Remote with name '${REMOTE_NAME}' not found. Check it exists and try again.`; + } + callback(error); + }); + }); +} + +function updateRepo(opts, callback) { + fetchRepo(opts, function(error) { + if(error) { + return callback(error); + } + checkOptions(opts, 'update', function(error) { + if(error) { + return callback(error); + } + var shortDir = opts.directory.replace(configuration.serverRoot, '') || opts.directory; + showSpinner(`Updating ${shortDir} to ${opts.revision}`); + + execCommand(`git reset --hard && git checkout ${opts.revision}`, { + cwd: opts.directory + }, function(error) { + hideSpinner(); + if(error) { + return callback(error); + } + log(`${shortDir} switched to revision ${opts.revision}`); + callback(); + }); + }); + }); +} + +/** +* Uses adapt.json to install the latest plugin versions +*/ +function updateFrameworkPlugins(opts, callback) { + if(arguments.length !== 2) { + return callback('Cannot update Adapt framework plugins, invalid options passed.'); + } + if(!opts.directory) { + return callback('Cannot update Adapt framework plugins, no target directory specified.'); + } + fs.readJSON(path.join(opts.directory, 'adapt.json'), function(error, json) { + if (error) { + return callback(error); + } + var plugins = Object.keys(json.dependencies); + async.eachSeries(plugins, function(plugin, pluginCallback) { + var _done = function() { + hideSpinner(); + pluginCallback.apply(this, arguments); + }; + showSpinner(`Updating Adapt framework plugin '${plugin}'`); + + if(json.dependencies[plugin] === '*') { + app.bowermanager.installLatestCompatibleVersion(plugin, _done); + } else { + app.bowermanager.installPlugin(plugin, json.dependencies[plugin], _done); + } + }, function(error) { + hideSpinner(); + if(error) { + return callback(error); + } + log('Adapt framework plugins updated.'); + callback(); + }); + }); +} + +/** +* This isn't used by the authoring tool +*/ +function purgeCourseFolder(opts, callback) { + if(arguments.length !== 2) { + return callback('Cannot remove course folder, invalid options passed.'); + } + if(!opts.directory) { + return callback('Cannot remove course folder, no target directory specified.'); + } + fs.remove(path.join(opts.directory, 'src', 'course'), callback); +} + +function updateAuthoring(opts, callback) { + if(!opts.revision) { + return callback('Cannot update server, revision not specified.'); + } + if(!opts.repository) { + opts.repository = DEFAULT_SERVER_REPO; + } + async.series([ + function fetchLatest(cb) { + fetchRepo(opts, cb); + }, + function pullLatest(cb) { + updateRepo(opts, cb); + }, + function installDeps(cb) { + installDependencies(cb); + }, + function rebuildApp(cb) { + buildAuthoring(cb); + } + ], function(error) { + if(!error) { + log(`Server has been updated successfully!`); + } + callback(error); + }); +} + +function buildAuthoring(callback) { + showSpinner('Building web application'); + execCommand('grunt build:prod', function(error){ + hideSpinner(); + if(error) { + return callback(error); + } + log('Web application built successfully.'); + callback(); + }); +} + +function installDependencies(opts, callback) { + if(arguments.length === 1) { + callback = opts; + } + showSpinner(`Installing node dependencies`); + + var cwd = opts.directory || configuration.serverRoot; + + fs.remove(path.join(cwd, 'node_modules'), function(error) { + if(error) { + return callback(error); + } + execCommand('npm install --production', { cwd: cwd }, function(error) { + hideSpinner(); + if(error) { + return callback(error); + } + log('Node dependencies installed successfully.'); + callback(); + }); + }); +} + +function execCommand(cmd, opts, callback) { + if(arguments.length === 2) { + callback = opts; + opts = {}; + } + var stdoutData = ''; + var errData = ''; + var child = exec(cmd, _.extend({ stdio: [0, 'pipe', 'pipe'] }, opts)); + child.stdout.on('data', function(data) { stdoutData += data; }); + child.stderr.on('data', function(data) { errData += data; }); + child.on('exit', function(error) { + if(error) { + return callback(errData || error); + } + callback(null, stdoutData); + }); +} + +function log(msg) { + if(!isSilent()) console.log(msg); +} + +function isSilent() { + return process.env.SILENT; +} diff --git a/lib/tenantmanager.js b/lib/tenantmanager.js index 942ff280bf..18ea4e3798 100644 --- a/lib/tenantmanager.js +++ b/lib/tenantmanager.js @@ -11,7 +11,7 @@ var util = require('util'); var configuration = require('./configuration'); var database = require('./database'); -var frameworkhelper = require('./frameworkhelper'); +var installHelpers = require('./installHelpers'); var logger = require('./logger'); // Constants @@ -235,50 +235,35 @@ exports = module.exports = { * @param {function} callback - function of the form function (error, tenant) */ createTenantFilesystem: function(tenant, callback) { - function copyFramework(callback) { - logger.log('info', 'Copying Adapt framework into place for new tenant'); - ncp(path.join(configuration.serverRoot, FRAMEWORK_DIR), path.join(configuration.tempDir, tenant._id.toString(), FRAMEWORK_DIR), function (err) { - if (err) { - logger.log('error', err); - return callback(err); - } else { - return callback(null); - } - }); - }; - - if (tenant.isMaster) { - logger.log('info', 'Creating master tenant filesystem'); + if (!tenant.isMaster) { + logger.log('info', 'No filesystem required for tenant ' + tenant.name); + callback(null); + } + logger.log('info', 'Creating master tenant filesystem'); - mkdirp(path.join(configuration.tempDir, tenant._id.toString()), function (err) { - if (err) { - logger.log('error', err); - return callback(err); + mkdirp(path.join(configuration.tempDir, tenant._id.toString()), function (err) { + if (err) { + logger.log('error', err); + return callback(err); + } + var tenantFrameworkDir = path.join(configuration.tempDir, tenant._id.toString(), FRAMEWORK_DIR); + fs.exists(tenantFrameworkDir, function(exists) { + if (exists) { + return callback(); } - - // Check that framework exists - fs.exists(path.join(configuration.serverRoot, FRAMEWORK_DIR), function(exists) { - if (!exists) { - logger.log('info', 'Framework does not exist'); - frameworkhelper.cloneFramework(function(err, result) { - if (err) { - logger.log('fatal', 'Error cloning framework', err); - return callback(err); - } - else { - return copyFramework(callback) - } - }); - } else { - return copyFramework(callback); + logger.log('info', 'Framework does not exist, will download'); + installHelpers.installFramework({ + repository: configuration.getConfig('frameworkRepository'), + revision: configuration.getConfig('frameworkRevision'), + directory: tenantFrameworkDir + }, function(err, result) { + if (err) { + logger.log('error', `Error downloading the framework ${err}`); } + callback(err); }); }); - } else { - logger.log('info', 'No filesystem required for tenant ' + tenant.name); - callback(null); - } - + }); }, /** @@ -346,6 +331,11 @@ exports = module.exports = { logger.log('error', 'Failed to create tenant: ', tenant); return callback(error); } + // these are needed in createTenantFilesystem + if(tenant.isMaster) { + configuration.setConfig('masterTenantName', result.name); + configuration.setConfig('masterTenantID', result._id.toString()); + } self.createTenantFilesystem(result, function(error) { if (error) { diff --git a/package.json b/package.json index 13cf898a36..930d7c3809 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,18 @@ "adapt", "authoring" ], + "repository": { + "type": "git", + "url": "https://github.com/adaptlearning/adapt_authoring.git" + }, + "framework": "2", + "main": "index", + "engines": { + "node": "4.x" + }, + "scripts": { + "test": "grunt test" + }, "contributors": [ { "name": "Ryan Adams", @@ -77,17 +89,6 @@ "email": "nicola.willis@canstudios.com" } ], - "repository": { - "type": "git", - "url": "https://github.com/adaptlearning/adapt_authoring.git" - }, - "main": "index", - "engines": { - "node": "4.x" - }, - "scripts": { - "test": "grunt test" - }, "dependencies": { "archiver": "~0.16.0", "async": "2.5.0", @@ -126,6 +127,7 @@ "json-schema-mapper": "0.0.2", "junk": "^1.0.2", "less": "^2.7.1", + "log-update": "^2.1.0", "matchdep": "~0.3.0", "method-override": "^2.3.5", "mime": "1.2.x", @@ -142,7 +144,7 @@ "nodemailer": "~2.5.0", "optimist": "*", "passport": "~0.1.17", - "prompt": "0.2.14", + "prompt": "^1.0.0", "request": "^2.53.0", "rimraf": "~2.2.5", "semver": "^5.0.3", diff --git a/plugins/content/bower/index.js b/plugins/content/bower/index.js index fdc2d273ba..3811d84bee 100644 --- a/plugins/content/bower/index.js +++ b/plugins/content/bower/index.js @@ -33,7 +33,7 @@ var origin = require('../../../'), unzip = require('unzip'), exec = require('child_process').exec, IncomingForm = require('formidable').IncomingForm, - version = require('../../../version.json'); + installHelpers = require('../../../lib/installHelpers'); // errors function PluginPackageError (msg) { @@ -776,22 +776,22 @@ BowerPlugin.prototype.updatePackages = function (plugin, options, cb) { }) .on('end', function (packageInfo) { // add details for each to the db - async.eachSeries( - Object.keys(packageInfo), - function (key, next) { - if (packageInfo[key].pkgMeta.framework) { - // If the plugin defines a framework, ensure that it is compatible - if (semver.satisfies(semver.clean(version.adapt_framework), packageInfo[key].pkgMeta.framework)) { - addPackage(plugin, packageInfo[key], options, next); - } else { - logger.log('warn', 'Unable to install ' + packageInfo[key].pkgMeta.name + ' as it is not supported in the current version of of the Adapt framework'); - next(); - } - } else { - addPackage(plugin, packageInfo[key], options, next); + async.eachSeries(Object.keys(packageInfo), function (key, next) { + if (!packageInfo[key].pkgMeta.framework) { + return addPackage(plugin, packageInfo[key], options, next); + } + installHelpers.getInstalledFrameworkVersion(function(error, frameworkVersion) { + if(error) { + return next(error); + } + // If the plugin defines a framework, ensure that it is compatible + if (!semver.satisfies(semver.clean(frameworkVersion), packageInfo[key].pkgMeta.framework)) { + logger.log('warn', 'Unable to install ' + packageInfo[key].pkgMeta.name + ' as it is not supported in the current version of the Adapt framework'); + return next(); } - }, - cb); + addPackage(plugin, packageInfo[key], options, next); + }); + }, cb); }); }); }); @@ -831,21 +831,26 @@ function checkIfHigherVersionExists (package, options, cb) { logger.log('error', `Unexpected number of ${packageName}s found (${results.length})`); return cb(error); } - var installedVersion = results[0].version; - var latestVersionIsNewer = semver.gt(latestPkg.version, installedVersion); - var satisfiesFrameworkReq = semver.satisfies(semver.clean(version.adapt_framework), latestPkg.framework); + installHelpers.getInstalledFrameworkVersion(function(error, frameworkVersion) { + if(error) { + return cb(error); + } + var installedVersion = results[0].version; + var latestVersionIsNewer = semver.gt(latestPkg.version, installedVersion); + var satisfiesFrameworkReq = semver.satisfies(semver.clean(frameworkVersion), latestPkg.framework); - if(!latestVersionIsNewer) { - logger.log('info', `Already using the latest version of ${packageName} (${latestPkg.version})`); - return cb(null, false); - } - if(!satisfiesFrameworkReq) { - // TODO recursively check old versions; we may be several releases behind - logger.log('warn', `A later version of ${packageName} is available but is not supported by the installed version of the Adapt framework (${version.adapt_framework})`); - return cb(null, false); - } - logger.log('info', `A new version of ${packageName} is available (${latestPkg.version})`); - cb(null, true); + if(!latestVersionIsNewer) { + logger.log('info', `Already using the latest version of ${packageName} (${latestPkg.version})`); + return cb(null, false); + } + if(!satisfiesFrameworkReq) { + // TODO recursively check old versions; we may be several releases behind + logger.log('warn', `A later version of ${packageName} is available but is not supported by the installed version of the Adapt framework (${frameworkVersion})`); + return cb(null, false); + } + logger.log('info', `A new version of ${packageName} is available (${latestPkg.version})`); + cb(null, true); + }); }); }); }) @@ -937,23 +942,25 @@ function handleUploadedPlugin (req, res, next) { pkgMeta: packageJson }; - // Check if the framework has been defined on the plugin and that it's not compatible - if (packageInfo.pkgMeta.framework && !semver.satisfies(semver.clean(version.adapt_framework), packageInfo.pkgMeta.framework)) { - return next(new PluginPackageError('This plugin is incompatible with version ' + version.adapt_framework + ' of the Adapt framework')); - } - - app.contentmanager.getContentPlugin(pluginType, function (error, contentPlugin) { - if (error) { + installHelpers.getInstalledFrameworkVersion(function(error, frameworkVersion) { + if(error) { return next(error); } - - addPackage(contentPlugin.bowerConfig, packageInfo, { strict: true }, function (error, results) { + // Check if the framework has been defined on the plugin and that it's not compatible + if (packageInfo.pkgMeta.framework && !semver.satisfies(semver.clean(frameworkVersion), packageInfo.pkgMeta.framework)) { + return next(new PluginPackageError('This plugin is incompatible with version ' + frameworkVersion + ' of the Adapt framework')); + } + app.contentmanager.getContentPlugin(pluginType, function (error, contentPlugin) { if (error) { return next(error); } - - res.statusCode = 200; - return res.json({ success: true, pluginType: pluginType, message: 'successfully added new plugin' }); + addPackage(contentPlugin.bowerConfig, packageInfo, { strict: true }, function (error, results) { + if (error) { + return next(error); + } + res.statusCode = 200; + return res.json({ success: true, pluginType: pluginType, message: 'successfully added new plugin' }); + }); }); }); diff --git a/plugins/output/adapt/index.js b/plugins/output/adapt/index.js index 9e0e46ce86..bc8c1a1c50 100644 --- a/plugins/output/adapt/index.js +++ b/plugins/output/adapt/index.js @@ -23,7 +23,7 @@ var origin = require('../../../'), assetmanager = require('../../../lib/assetmanager'), exec = require('child_process').exec, semver = require('semver'), - version = require('../../../version'), + installHelpers = require('../../../lib/installHelpers'), logger = require('../../../lib/logger'); function AdaptOutput() { @@ -39,7 +39,8 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next outputJson = {}, isRebuildRequired = false, themeName = '', - menuName = Constants.Defaults.MenuName; + menuName = Constants.Defaults.MenuName, + frameworkVersion; var resultObject = {}; @@ -146,10 +147,15 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next if (err) { return callback(err); } - callback(null); }); }, + function(callback) { + installHelpers.getInstalledFrameworkVersion(function(error, version) { + frameworkVersion = version; + callback(error); + }); + }, function(callback) { fs.exists(path.join(BUILD_FOLDER, Constants.Filenames.Main), function(exists) { if (!exists || isRebuildRequired) { @@ -159,7 +165,7 @@ AdaptOutput.prototype.publish = function(courseId, mode, request, response, next var outputFolder = COURSE_FOLDER.replace(FRAMEWORK_ROOT_FOLDER + path.sep,''); // Append the 'build' folder to later versions of the framework - if (semver.gte(semver.clean(version.adapt_framework), semver.clean('2.0.0'))) { + if (semver.gte(semver.clean(frameworkVersion), semver.clean('2.0.0'))) { outputFolder = path.join(outputFolder, Constants.Folders.Build); } diff --git a/test/entry.js b/test/entry.js index 75362309bf..1611ee2150 100644 --- a/test/entry.js +++ b/test/entry.js @@ -20,6 +20,8 @@ var EXTENDED_TIMEOUT = 600000; before(function(done) { this.timeout(EXTENDED_TIMEOUT); + process.env.SILENT = true; + async.series([ removeTestData, function initApp(cb) { diff --git a/upgrade.js b/upgrade.js index e9ac704f0e..2a4f4d8432 100644 --- a/upgrade.js +++ b/upgrade.js @@ -1,397 +1,158 @@ -var builder = require('./lib/application'); -var prompt = require('prompt'); -var fs = require('fs'); -var request = require('request'); +var _ = require('underscore'); var async = require('async'); -var exec = require('child_process').exec; -var rimraf = require('rimraf'); -var path = require('path'); +var chalk = require('chalk'); +var fs = require('fs-extra'); var optimist = require('optimist'); +var path = require('path'); +var semver = require('semver'); -// Constants -var DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'; - -// Helper -var isVagrant = function () { - if (process.argv.length > 2) { - return true; - } - return false; -}; - -// GLOBALS -var app = builder(); -var installedBuilderVersion = ''; -var latestBuilderTag = ''; -var installedFrameworkVersion = ''; -var latestFrameworkTag = ''; -var shouldUpdateBuilder = false; -var shouldUpdateFramework = false; -var versionFile = JSON.parse(fs.readFileSync('version.json'), {encoding: 'utf8'}); -var configFile = JSON.parse(fs.readFileSync('conf/config.json'), {encoding: 'utf8'}); - -var steps = [ - function(callback) { - - console.log('Checking versions'); - - if (versionFile) { - installedBuilderVersion = versionFile.adapt_authoring; - installedFrameworkVersion = versionFile.adapt_framework; - } - - console.log('Currently installed versions:\n- ' + app.polyglot.t('app.productname') + ': ' + installedBuilderVersion + '\n- Adapt Framework: ' + installedFrameworkVersion); - callback(); - - }, - function(callback) { - - console.log('Checking for ' + app.polyglot.t('app.productname') + ' upgrades...'); - // Check the latest version of the project - request({ - headers: { - 'User-Agent' : DEFAULT_USER_AGENT - }, - uri: 'https://api.github.com/repos/adaptlearning/adapt_authoring/tags', - method: 'GET' - }, function (error, response, body) { +var configuration = require('./lib/configuration'); +var logger = require('./lib/logger'); +var origin = require('./lib/application'); +var OutputConstants = require('./lib/outputmanager').Constants; +var installHelpers = require('./lib/installHelpers'); - if (!error && response.statusCode == 200) { - var tagInfo = JSON.parse(body); +var DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36'; +var app = origin(); - if (tagInfo) { - latestBuilderTag = tagInfo[0].name; - } +/** +* Start of execution +*/ +start(); + +function start() { + // don't show any logger messages in the console + logger.level('console','error'); + // start the server first + app.run({ skipVersionCheck: true, skipStartLog: true }); + app.on('serverStarted', getUserInput); +} - callback(); +function getUserInput() { + // properties for the prompts + var confirmProperties = { + name: 'continue', + description: 'Continue? Y/n', + type: 'string', + default: 'Y', + before: installHelpers.inputHelpers.toBoolean + }; + var upgradeProperties = { + properties: { + updateAutomatically: { + description: 'Update automatically? Y/n', + type: 'string', + default: 'Y', + before: installHelpers.inputHelpers.toBoolean } - - }); - - }, - function(callback) { - - console.log('Checking for Adapt Framework upgrades...'); - // Check the latest version of the framework - request({ - headers: { - 'User-Agent' : DEFAULT_USER_AGENT + } + }; + var tagProperties = { + properties: { + authoringToolGitTag: { + type: 'string', + description: 'Specific git revision to be used for the authoring tool. Accepts any valid revision type (e.g. branch/tag/commit)', + default: '' }, - uri: 'https://api.github.com/repos/adaptlearning/adapt_framework/tags', - method: 'GET' - }, function (error, response, body) { - if (!error && response.statusCode == 200) { - var tagInfo = JSON.parse(body); - - if (tagInfo) { - latestFrameworkTag = tagInfo[0].name; - } - - callback(); + frameworkGitTag: { + type: 'string', + description: 'Specific git revision to be used for the framework. Accepts any valid revision type (e.g. branch/tag/commit)', + default: '' } - }); - - }, - function(callback) { - // Check what needs upgrading - if (latestBuilderTag != installedBuilderVersion) { - shouldUpdateBuilder = true; - console.log('Update for ' + app.polyglot.t('app.productname') + ' is available: ' + latestBuilderTag); - } - - if (latestFrameworkTag != installedFrameworkVersion) { - shouldUpdateFramework = true; - console.log('Update for Adapt Framework is available: ' + latestFrameworkTag); - } - - // If neither of the Builder or Framework need updating then quit the upgrading process - if (!shouldUpdateFramework && !shouldUpdateBuilder) { - console.log('No updates available at this time\n'); - process.exit(0); - } - - callback(); - - }, function(callback) { - // Upgrade Builder if we need to - if (shouldUpdateBuilder) { - - upgradeBuilder(latestBuilderTag, function(err) { - if (err) { - return callback(err); - } - - versionFile.adapt_authoring = latestBuilderTag; - callback(); - - }); - - } else { - callback(); } - - }, function(callback) { - - // Upgrade Framework if we need to - if (shouldUpdateFramework) { - - upgradeFramework(latestFrameworkTag, function(err) { - if (err) { - return callback(err); - } - - versionFile.adapt_framework = latestFrameworkTag; - callback(); - - }); - - } else { - callback(); + }; + console.log(`\nThis script will update the ${app.polyglot.t('app.productname')} and/or Adapt Framework. Would you like to continue?`); + installHelpers.getInput(confirmProperties, function(result) { + if(!result.continue) { + return installHelpers.exit(); } - - }, function(callback) { - - // After upgrading let's update the version.json to the latest version - fs.writeFile('version.json', JSON.stringify(versionFile, null, 4), function(err) { - if(err) { - callback(err); - } else { - console.log("Version file updated\n"); - callback(); - } - }); - - }, function(callback) { - if (shouldUpdateFramework) { - // If the framework has been updated, interrogate the adapt.json file from the adapt_framework - // folder and install the latest versions of the core plugins - fs.readFile(path.join(configFile.root, 'temp', configFile.masterTenantID, 'adapt_framework', 'adapt.json'), function (err, data) { - if (err) { - return callback(err); - } - - var json = JSON.parse(data); - // 'dependencies' contains a key-value pair representing the plugin name and the semver - var plugins = Object.keys(json.dependencies); - - async.eachSeries(plugins, function(plugin, pluginCallback) { - app.bowermanager.installPlugin(plugin, json.dependencies[plugin], function(err) { - if (err) { - return pluginCallback(err); - } - - pluginCallback(); - }); - - }, function(err) { - if (err) { - console.log(err); - return callback(err); + installHelpers.getInput(upgradeProperties, function(result) { + console.log(''); + if(result.updateAutomatically) { + return checkForUpdates(function(error, updateData) { + if(error) { + return installHelpers.exit(1, error); } - - callback(); + doUpdate(updateData); }); - }); - } else { - callback(); - } - }, - function(callback) { - // Left empty for any upgrade scripts - just remember to call the callback when done. - callback(); - } -]; - -app.run({skipVersionCheck: true, skipStartLog: true}); - -app.on('serverStarted', function () { - prompt.override = optimist.argv; - prompt.start(); - - // Prompt the user to begin the install - if (isVagrant()) { - console.log(`\nUpdate the ${app.polyglot.t('app.productname')} (and/or Adapt Framework) to the latest released version.`); - } else { - console.log(`\nThis script will update the ${app.polyglot.t('app.productname')} (and/or Adapt Framework) to the latest released version. Would you like to continue?`); - } - - prompt.get({ name: 'Y/n', type: 'string', default: 'Y' }, function (err, result) { - if (!/(Y|y)[es]*$/.test(result['Y/n'])) { - return exitUpgrade(); - } - - // run steps - async.series(steps, function (err, results) { - - if (err) { - console.log('ERROR: ', err); - return exitUpgrade(1, 'Upgrade was unsuccessful. Please check the console output.'); } - - console.log(' '); - - exitUpgrade(0, 'Great work! Your ' + app.polyglot.t('app.productname') + ' is now updated.'); - }); - }); -}); - -// This upgrades the Builder -function upgradeBuilder(tagName, callback) { - - console.log('Upgrading the ' + app.polyglot.t('app.productname') + '...please hold on!'); - var child = exec('git fetch origin', { - stdio: [0, 'pipe', 'pipe'] - }); - - child.stdout.on('data', function(err) { - console.log(err); - }); - child.stderr.on('data', function(err) { - console.log(err); - }); - - child.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); - } - - console.log("Fetch from GitHub was successful."); - console.log("Pulling latest changes..."); - - var secondChild = exec('git reset --hard ' + tagName, { - stdio: [0, 'pipe', 'pipe'] - }); - - secondChild.stdout.on('data', function(err) { - console.log(err); - }); - - secondChild.stderr.on('data', function(err) { - console.log(err); - }); - - secondChild.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); - } - - console.log("Installing " + app.polyglot.t('app.productname') + " dependencies.\n"); - - var thirdChild = exec('npm install', { - stdio: [0, 'pipe', 'pipe'] - }); - - thirdChild.stdout.on('data', function(err) { - console.log(err); - }); - - thirdChild.stderr.on('data', function(err) { - console.log(err); - }); - - thirdChild.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); + // no automatic update, so get the intended versions + installHelpers.getInput(tagProperties, function(result) { + console.log(''); + if(!result.authoringToolGitTag && !result.frameworkGitTag) { + return installHelpers.exit(1, 'Cannot update sofware if no revisions are specified.'); } - console.log("Dependencies installed.\n"); - - console.log("Building front-end.\n"); - - var fourthChild = exec('grunt build:prod', { - stdio: [0, 'pipe', 'pipe'] - }); - - fourthChild.stdout.on('data', function(err) { - console.log(err); - }); - - fourthChild.stderr.on('data', function(err) { - console.log(err); - }); - - fourthChild.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); - } - - console.log("front-end built.\n"); - - console.log(app.polyglot.t('app.productname') + " has been updated.\n"); - callback(); + doUpdate({ + adapt_authoring: result.authoringToolGitTag, + adapt_framework: result.frameworkGitTag }); }); }); }); } -// This upgrades the Framework -function upgradeFramework(tagName, callback) { - console.log('Upgrading the Adapt Framework...please hold on!'); - - var child = exec('git fetch origin', { - cwd: 'temp/' + configFile.masterTenantID + '/adapt_framework', - stdio: [0, 'pipe', 'pipe'] - }); - - child.stdout.on('data', function(err) { - console.log(err); - }); - - child.stderr.on('data', function(err) { - console.log(err); - }); - - child.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); +function checkForUpdates(callback) { + var isCustomFramework = configuration.getConfig('frameworkRepository') !== installHelpers.DEFAULT_FRAMEWORK_REPO; + var isCustomServer = configuration.getConfig('authoringToolRepository') !== installHelpers.DEFAULT_SERVER_REPO; + if(isCustomFramework || isCustomServer) { + return callback('Cannot perform an automatic upgrade when custom repositories are used.'); + } + installHelpers.showSpinner('Checking for updates'); + installHelpers.getUpdateData(function(error, data) { + installHelpers.hideSpinner(); + if(error) { + return callback(error); } + if(!data) { + return installHelpers.exit(0, `Your software is already up-to-date, no need to upgrade.`); + } + console.log(chalk.underline('Software updates found.\n')); + callback(null, data); + }); +} - console.log("Fetch from GitHub was successful."); - console.log("Pulling latest changes..."); - - var secondChild = exec('git reset --hard ' + tagName + ' && npm install', { - cwd: 'temp/' + configFile.masterTenantID + '/adapt_framework', - stdio: [0, 'pipe', 'pipe'] - }); - - secondChild.stdout.on('data', function(err) { - console.log(err); - }); - - secondChild.stderr.on('data', function(err) { - console.log(err); - }); - - secondChild.on('exit', function (error, stdout, stderr) { - if (error) { - return console.log('ERROR: ' + error); +function doUpdate(data) { + async.series([ + function upgradeAuthoring(cb) { + if(!data.adapt_authoring) { + return cb(); } - - console.log("Framework has been updated.\n"); - - rimraf(configFile.root + '/temp/' + configFile.masterTenantID + '/adapt_framework/src/course', function(err) { - if (err) { - console.log(err); + installHelpers.updateAuthoring({ + repository: configuration.getConfig('authoringToolRepository'), + revision: data.adapt_authoring, + directory: configuration.serverRoot + }, function(error) { + if(error) { + console.log(`Failed to update ${configuration.serverRoot} to '${data.adapt_authoring}'`); + return cb(error); } - - callback(); + console.log(`${app.polyglot.t('app.productname')} upgraded to ${data.adapt_authoring}`); + cb(); }); - - }); - + }, + function upgradeFramework(cb) { + if(!data.adapt_framework) { + return cb(); + } + var dir = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), OutputConstants.Folders.Framework); + installHelpers.updateFramework({ + repository: configuration.getConfig('frameworkRepository'), + revision: data.adapt_framework, + directory: dir + }, function(error) { + if(error) { + console.log(`Failed to upgrade ${dir.replace(configuration.serverRoot, '')} to ${data.adapt_framework}`); + return cb(error); + } + console.log(`Adapt framework upgraded to ${data.adapt_framework}`); + cb(); + }); + }, + ], function(error) { + if(error) { + console.error('ERROR:', error); + return installHelpers.exit(1, 'Upgrade was unsuccessful. Please check the console output.'); + } + installHelpers.exit(0, `Your ${app.polyglot.t('app.productname')} was updated successfully.`); }); } - -/** - * Exits the install with some cleanup, should there be an error - * - * @param {int} code - * @param {string} msg - */ - -function exitUpgrade (code, msg) { - code = code || 0; - msg = msg || 'Bye!'; - console.log(msg); - process.exit(code); -} diff --git a/version.json b/version.json deleted file mode 100644 index c4a102800e..0000000000 --- a/version.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "adapt_authoring": "v0.3.1", - "adapt_framework": "v2.0.17" -}