From 61fe3d3d8b692996149f62ea88627d53b079ca18 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 6 Aug 2017 12:42:03 -0400 Subject: [PATCH] feat: npm install execute OS specific install script --- npm/configs/config.passive.yml | 154 ------------------ npm/install.js | 279 ++------------------------------- npm/package-lock.json | Bin 44197 -> 0 bytes npm/package.json | 15 +- 4 files changed, 15 insertions(+), 433 deletions(-) delete mode 100644 npm/configs/config.passive.yml delete mode 100644 npm/package-lock.json diff --git a/npm/configs/config.passive.yml b/npm/configs/config.passive.yml deleted file mode 100644 index 5ef4904b..00000000 --- a/npm/configs/config.passive.yml +++ /dev/null @@ -1,154 +0,0 @@ -####################################################################### -# Wiki.js - CONFIGURATION # -####################################################################### -# Full explanation + examples in the documentation: -# https://docs.requarks.io/wiki/install - -# --------------------------------------------------------------------- -# Title of this site -# --------------------------------------------------------------------- - -title: $(WIKI_TITLE) - -# --------------------------------------------------------------------- -# Full public path to the site, without the trailing slash -# --------------------------------------------------------------------- - -host: $(WIKI_HOST) - -# --------------------------------------------------------------------- -# Port the main server should listen to (80 by default) -# --------------------------------------------------------------------- - -port: $(PORT) - -# --------------------------------------------------------------------- -# Data Directories -# --------------------------------------------------------------------- - -paths: - repo: ./repo - data: ./data - -# --------------------------------------------------------------------- -# Upload Limits -# --------------------------------------------------------------------- -# In megabytes (MB) - -uploads: - maxImageFileSize: 3 - maxOtherFileSize: 100 - -# --------------------------------------------------------------------- -# Site Language -# --------------------------------------------------------------------- -# Possible values: en, es, fr, ko, ru or zh - -lang: $(WIKI_LANG) - -# --------------------------------------------------------------------- -# Site Authentication -# --------------------------------------------------------------------- - -public: $(WIKI_PUBLIC) - -auth: - defaultReadAccess: false - local: - enabled: true - google: - enabled: false - clientId: GOOGLE_CLIENT_ID - clientSecret: GOOGLE_CLIENT_SECRET - microsoft: - enabled: false - clientId: MS_APP_ID - clientSecret: MS_APP_SECRET - facebook: - enabled: false - clientId: FACEBOOK_APP_ID - clientSecret: FACEBOOK_APP_SECRET - github: - enabled: false - clientId: GITHUB_CLIENT_ID - clientSecret: GITHUB_CLIENT_SECRET - slack: - enabled: false - clientId: SLACK_CLIENT_ID - clientSecret: SLACK_CLIENT_SECRET - ldap: - enabled: false - url: ldap://serverhost:389 - bindDn: cn='root' - bindCredentials: BIND_PASSWORD - searchBase: o=users,o=example.com - searchFilter: (uid={{username}}) - tlsEnabled: false - tlsCertPath: C:\example\root_ca_cert.crt - azure: - enabled: false - clientID: APP_ID - clientSecret: APP_SECRET_KEY - resource: '00000002-0000-0000-c000-000000000000' - tenant: 'YOUR_TENANT.onmicrosoft.com' - -# --------------------------------------------------------------------- -# Secret key to use when encrypting sessions -# --------------------------------------------------------------------- -# Use a long and unique random string (256-bit keys are perfect!) - -sessionSecret: $(WIKI_SESSION_KEY) - -# --------------------------------------------------------------------- -# Database Connection String -# --------------------------------------------------------------------- - -db: $(MONGODB_URI) - -# --------------------------------------------------------------------- -# Git Connection Info -# --------------------------------------------------------------------- - -git: - url: $(WIKI_GIT_URL) - branch: $(WIKI_GIT_BRANCH) - auth: - - # Type: basic or ssh - type: basic - - # Only for Basic authentication: - username: $(WIKI_GIT_USERNAME) - password: $(WIKI_GIT_PASSWORD) - - # Only for SSH authentication: - privateKey: /etc/wiki/keys/git.pem - - sslVerify: true - - # Default email to use as commit author - serverEmail: $(WIKI_SERVER_EMAIL) - - # Whether to use user email as author in commits - showUserEmail: $(WIKI_SHOW_USER_EMAIL) - -# --------------------------------------------------------------------- -# Features -# --------------------------------------------------------------------- -# You can enable / disable specific features below - -features: - linebreaks: true - mathjax: true - -# --------------------------------------------------------------------- -# External Logging -# --------------------------------------------------------------------- - -externalLogging: - bugsnag: false - loggly: false - papertrail: false - rollbar: false - sentry: false - diff --git a/npm/install.js b/npm/install.js index 9be65379..cd0a0511 100644 --- a/npm/install.js +++ b/npm/install.js @@ -5,274 +5,21 @@ // Installation Script // ===================================================== -const Promise = require('bluebird') -const _ = require('lodash') -const colors = require('colors/safe') -const exec = require('execa') -const fs = Promise.promisifyAll(require('fs-extra')) -const https = require('follow-redirects').https -const inquirer = require('inquirer') -const os = require('os') const path = require('path') -const pm2 = Promise.promisifyAll(require('pm2')) -const tar = require('tar') -const zlib = require('zlib') - +const spawn = require('child_process').spawn const installDir = path.resolve(__dirname, '../..') -const isContainerBased = (process.env.WIKI_JS_HEROKU || process.env.WIKI_JS_DOCKER) -let installMode = 'new' +const cmd = (process.platform !== 'win32') + ? 'curl -s -S -o- https://wiki.js.org/install.sh | bash' + : `PowerShell.exe -NoProfile -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://wiki.js.org/install.ps1'))"` -// ===================================================== -// INSTALLATION TASKS -// ===================================================== +console.info(`Executing installation script for ${process.platform} platform...`) -const tasks = { - /** - * Stop and delete existing instances - */ - stopAndDeleteInstances() { - ora.text = 'Looking for running instances...' - return pm2.connectAsync().then(() => { - return pm2.describeAsync('wiki').then(() => { - ora.text = 'Stopping and deleting process from pm2...' - return pm2.deleteAsync('wiki') - }).catch(err => { // eslint-disable-line handle-callback-err - return true - }).finally(() => { - pm2.disconnect() - }) - }).catch(err => { // eslint-disable-line handle-callback-err - return true - }) - }, - /** - * Check for sufficient memory - */ - checkRequirements() { - ora.text = 'Checking system requirements...' - if (os.totalmem() < 1000 * 1000 * 768) { - return Promise.reject(new Error('Not enough memory to install dependencies. Minimum is 768 MB.')) - } else { - return Promise.resolve(true) - } - }, - /** - * Install via local tarball if present - */ - installFromLocal() { - let hasTarball = true - let tbPath = path.join(installDir, 'wiki-js.tar.gz') - return fs.accessAsync(tbPath) - .catch(err => { // eslint-disable-line handle-callback-err - hasTarball = false - }).then(() => { - if (hasTarball) { - ora.text = 'Local tarball found. Extracting...' - - return new Promise((resolve, reject) => { - fs.createReadStream(tbPath).pipe(zlib.createGunzip()) - .pipe(tar.extract({ cwd: installDir })) - .on('error', err => reject(err)) - .on('end', () => { - ora.text = 'Tarball extracted successfully.' - resolve(tasks.installDependencies()) - isContainerBased && console.info('>> Installing dependencies...') - }) - }) - } else { - return false - } - }) - }, - /** - * Install from GitHub release download - */ - installFromRemote() { - // Fetch version from npm package - return fs.readJsonAsync('package.json').then((packageObj) => { - let versionGet = _.chain(packageObj.version).split('.').take(4).join('.') - let remoteURLApp = _.replace('https://github.com/Requarks/wiki/releases/download/v{0}/wiki-js.tar.gz', '{0}', versionGet) - let remoteURLDeps = _.replace('https://github.com/Requarks/wiki/releases/download/v{0}/node_modules.tar.gz', '{0}', versionGet) - - return new Promise((resolve, reject) => { - // Fetch app tarball - ora.text = 'Looking for app package...' - https.get(remoteURLApp, resp => { - if (resp.statusCode !== 200) { - return reject(new Error('Remote file not found')) - } - ora.text = 'Remote app tarball found. Downloading...' - isContainerBased && console.info('>> Extracting app to ' + installDir) - - // Extract app tarball - resp.pipe(zlib.createGunzip()) - .pipe(tar.extract({ cwd: installDir })) - .on('error', err => reject(err)) - .on('end', () => { - ora.text = 'App tarball extracted successfully.' - resolve(true) - }) - }) - }).then(() => { - return new Promise((resolve, reject) => { - // Fetch deps tarball - ora.text = 'Looking for dependencies package...' - https.get(remoteURLDeps, resp => { - if (resp.statusCode !== 200) { - return reject(new Error('Remote file not found')) - } - ora.text = 'Remote dependencies tarball found. Downloading...' - isContainerBased && console.info('>> Extracting dependencies to ' + installDir) - - // Extract deps tarball - resp.pipe(zlib.createGunzip()) - .pipe(tar.extract({ cwd: path.join(installDir, 'node_modules') })) - .on('error', err => reject(err)) - .on('end', () => { - ora.text = 'Dependencies tarball extracted successfully.' - resolve(true) - }) - }) - }) - }) - }) - }, - /** - * Create default config.yml file if new installation - */ - ensureConfigFile() { - return fs.accessAsync(path.join(installDir, 'config.yml')).then(() => { - // Is Upgrade - ora.text = 'Existing config.yml found. Upgrade mode.' - installMode = 'upgrade' - return true - }).catch(err => { - // Is New Install - if (err.code === 'ENOENT') { - ora.text = 'First-time install, creating a new config.yml...' - installMode = 'new' - let sourceConfigFile = path.join(installDir, 'config.sample.yml') - if (process.env.WIKI_JS_HEROKU || process.env.WIKI_JS_DOCKER) { - sourceConfigFile = path.join(__dirname, 'configs/config.passive.yml') - } - return fs.copyAsync(sourceConfigFile, path.join(installDir, 'config.yml')) - } else { - return err - } - }) - }, - /** - * Install npm dependencies - */ - installDependencies() { - ora.text = 'Installing Wiki.js npm dependencies...' - return exec.stdout('npm', ['install', '--only=production', '--no-optional'], { - cwd: installDir - }).then(results => { - ora.text = 'Wiki.js npm dependencies installed successfully.' - return true - }) - }, - startConfigurationWizard() { - ora.succeed('Installation succeeded.') - if (process.env.WIKI_JS_HEROKU) { - console.info('> Wiki.js has been installed and is configured to use Heroku configuration.') - return true - } else if (process.env.WIKI_JS_DOCKER) { - console.info('Docker environment detected. Skipping setup wizard auto-start.') - return true - } else if (process.stdout.isTTY) { - if (installMode === 'upgrade') { - console.info(colors.yellow('\n!!! IMPORTANT !!!')) - console.info(colors.yellow('Running the configuration wizard is optional but recommended after an upgrade to ensure your config file is using the latest available settings.')) - console.info(colors.yellow('Note that the contents of your config file will be displayed during the configuration wizard. It is therefor highly recommended to run the wizard on a non-publicly accessible port or skip this step completely.\n')) - } - inquirer.prompt([{ - type: 'list', - name: 'action', - message: 'Continue with configuration wizard?', - default: 'default', - choices: [ - { name: 'Yes, run configuration wizard on port 3000 (recommended)', value: 'default', short: 'Yes' }, - { name: 'Yes, run configuration wizard on a custom port...', value: 'custom', short: 'Yes' }, - { name: 'No, I\'ll configure the config file manually', value: 'exit', short: 'No' } - ] - }, { - type: 'input', - name: 'customport', - message: 'Custom port to use:', - default: 3000, - validate: (val) => { - val = _.toNumber(val) - return (_.isNumber(val) && val > 0) ? true : 'Invalid Port!' - }, - when: (ans) => { - return ans.action === 'custom' - } - }]).then((ans) => { - switch (ans.action) { - case 'default': - console.info(colors.bold.cyan('> Browse to http://your-server:3000/ to configure your wiki! (Replaced your-server with the hostname or IP of your server!)')) - ora = require('ora')({ text: 'I\'ll wait until you\'re done ;)', color: 'yellow', spinner: 'pong' }).start() - return exec.stdout('node', ['wiki', 'configure'], { - cwd: installDir - }) - case 'custom': - console.info(colors.bold.cyan('> Browse to http://your-server:' + ans.customport + '/ to configure your wiki! (Replaced your-server with the hostname or IP of your server!)')) - ora = require('ora')({ text: 'I\'ll wait until you\'re done ;)', color: 'yellow', spinner: 'pong' }).start() - return exec.stdout('node', ['wiki', 'configure', ans.customport], { - cwd: installDir - }) - default: - console.info(colors.bold.cyan('\n> You can run the configuration wizard using command:') + colors.bold.white(' node wiki configure') + colors.bold.cyan('.\n> Then start Wiki.js using command: ') + colors.bold.white('node wiki start')) - return Promise.delay(7000).then(() => { - process.exit(0) - }) - } - }).then(() => { - ora.succeed(colors.bold.green('Wiki.js has been configured successfully. It is now starting up and should be accessible very soon!')) - return Promise.delay(3000).then(() => { - console.info('npm is finishing... please wait...') - }) - }) - } else { - console.info(colors.cyan('[WARNING] Non-interactive terminal detected. You must manually start the configuration wizard using the command: node wiki configure')) - } - } -} - -// ===================================================== -// INSTALL SEQUENCE -// ===================================================== - -if (!isContainerBased) { - console.info(colors.yellow( - ' __ __ _ _ _ _ \n' + - '/ / /\\ \\ (_) | _(_) (_)___ \n' + - '\\ \\/ \\/ / | |/ / | | / __| \n' + - ' \\ /\\ /| | <| |_ | \\__ \\ \n' + - ' \\/ \\/ |_|_|\\_\\_(_)/ |___/ \n' + - ' |__/\n')) -} else { - console.info('> WIKI.JS [Installing...]') -} - -let ora = require('ora')({ text: 'Initializing...', spinner: 'dots12' }).start() - -Promise.join( - tasks.stopAndDeleteInstances(), - tasks.checkRequirements() -).then(() => { - isContainerBased && console.info('>> Fetching tarball...') - return tasks.installFromLocal().then(succeeded => { - return (!succeeded) ? tasks.installFromRemote() : true - }) -}).then(() => { - isContainerBased && console.info('>> Creating config file...') - return tasks.ensureConfigFile() -}).then(() => { - return tasks.startConfigurationWizard() -}).catch(err => { - isContainerBased && console.error(err) - ora.fail(err) +let inst = spawn(cmd, [], { + cwd: installDir, + env: process.env, + shell: true, + stdio: 'inherit', + detached: true }) + +inst.unref() diff --git a/npm/package-lock.json b/npm/package-lock.json deleted file mode 100644 index 7a19806a378a3bd067b493ca074b6a78129e057c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44197 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 eoYvxcUKy$Gbg}}+Z$6IvK)uGl|Ks2P@&5p