install.js 8.64 KB
Newer Older
NGPixel's avatar
NGPixel committed
1 2
'use strict'

3 4 5 6 7
// =====================================================
// Wiki.js
// Installation Script
// =====================================================

NGPixel's avatar
NGPixel committed
8
const Promise = require('bluebird')
9 10
const _ = require('lodash')
const colors = require('colors/safe')
11
const exec = require('execa')
NGPixel's avatar
NGPixel committed
12 13
const fs = Promise.promisifyAll(require('fs-extra'))
const https = require('follow-redirects').https
14 15
const inquirer = require('inquirer')
const os = require('os')
NGPixel's avatar
NGPixel committed
16 17 18 19 20
const path = require('path')
const pm2 = Promise.promisifyAll(require('pm2'))
const tar = require('tar')
const zlib = require('zlib')

21
const installDir = path.resolve(__dirname, '../..')
NGPixel's avatar
NGPixel committed
22

23 24 25
// =====================================================
// INSTALLATION TASKS
// =====================================================
26

27
const tasks = {
28 29 30
  /**
   * Stop and delete existing instances
   */
31 32 33 34 35 36 37 38 39 40 41 42 43
  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()
      })
    })
  },
44 45 46
  /**
   * Check for sufficient memory
   */
47 48 49 50 51 52 53 54
  checkRequirements () {
    ora.text = 'Checking system requirements...'
    if (os.totalmem() < 1024 * 1024 * 768) {
      return Promise.reject(new Error('Not enough memory to install dependencies. Minimum is 768 MB.'))
    } else {
      return Promise.resolve(true)
    }
  },
55 56 57
  /**
   * Install via local tarball if present
   */
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
  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({ path: installDir }))
              .on('error', err => reject(err))
              .on('end', () => {
                ora.text = 'Tarball extracted successfully.'
                resolve(true)
              })
          })
        } 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 remoteURL = _.replace('https://github.com/Requarks/wiki/releases/download/v{0}/wiki-js.tar.gz', '{0}', versionGet)
90 91

      return new Promise((resolve, reject) => {
92 93 94 95 96 97 98 99 100 101
        // Fetch tarball
        ora.text = 'Looking for latest release...'
        https.get(remoteURL, resp => {
          if (resp.statusCode !== 200) {
            return reject(new Error('Remote file not found'))
          }
          ora.text = 'Remote wiki.js tarball found. Downloading...'

          // Extract tarball
          resp.pipe(zlib.createGunzip())
102 103 104 105 106 107
          .pipe(tar.Extract({ path: installDir }))
          .on('error', err => reject(err))
          .on('end', () => {
            ora.text = 'Tarball extracted successfully.'
            resolve(true)
          })
108
        })
109
      })
110 111
    })
  },
112
  /**
113
   * Install npm dependencies
114
   */
115 116 117 118 119 120 121 122 123
  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
    })
  },
124
  /**
125
   * Create default config.yml file if new installation
126
   */
127 128 129 130 131 132 133 134 135 136 137 138
  ensureConfigFile () {
    return fs.accessAsync(path.join(installDir, 'config.yml')).then(() => {
      // Is Upgrade
      ora.succeed('Upgrade completed.')
      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'))
      return true
    }).catch(err => {
      // Is New Install
      if (err.code === 'ENOENT') {
        ora.text = 'First-time install, creating a new config.yml...'
139 140 141 142 143
        let sourceConfigFile = 'config.sample.yml'
        if (process.env.WIKI_JS_DOCKER) {
          sourceConfigFile = 'config.docker.yml'
        }
        return fs.copyAsync(path.join(installDir, sourceConfigFile), path.join(installDir, 'config.yml')).then(() => {
144 145
          ora.succeed('Installation succeeded.')
          return true
146
        })
147 148 149
      } else {
        return err
      }
NGPixel's avatar
NGPixel committed
150
    })
151 152
  },
  startConfigurationWizard () {
153
    if (process.stdout.isTTY && !process.env.WIKI_JS_DOCKER) {
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
      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':
NGPixel's avatar
NGPixel committed
179
            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!)'))
180 181 182 183 184
            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':
NGPixel's avatar
NGPixel committed
185
            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!)'))
186 187 188 189 190
            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:
191 192 193 194
            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)
            })
195 196
        }
      }).then(() => {
NGPixel's avatar
NGPixel committed
197
        ora.succeed(colors.bold.green('Wiki.js has been configured successfully. It is now starting up and should be accessible very soon!'))
198 199 200
        return Promise.delay(3000).then(() => {
          console.info('npm is finishing... please wait...')
        })
201 202
      })
    } else {
NGPixel's avatar
NGPixel committed
203
      console.info(colors.cyan('[WARNING] Non-interactive terminal detected. You must manually start the configuration wizard using the command: node wiki configure'))
204
    }
205 206 207 208 209 210 211
  }
}

// =====================================================
// INSTALL SEQUENCE
// =====================================================

212
if (!process.env.IS_HEROKU && !process.env.WIKI_JS_DOCKER) {
213 214 215 216 217 218 219 220
  console.info(colors.yellow(
    ' __    __ _ _    _    _     \n' +
    '/ / /\\ \\ (_) | _(_)  (_)___ \n' +
    '\\ \\/  \\/ / | |/ / |  | / __| \n' +
    ' \\  /\\  /| |   <| |_ | \\__ \\ \n' +
    '  \\/  \\/ |_|_|\\_\\_(_)/ |___/ \n' +
    '                   |__/\n'))
}
221 222 223 224 225 226 227 228 229

let ora = require('ora')({ text: 'Initializing...', spinner: 'dots12' }).start()

Promise.join(
  tasks.stopAndDeleteInstances(),
  tasks.checkRequirements()
).then(() => {
  return tasks.installFromLocal().then(succeeded => {
    return (!succeeded) ? tasks.installFromRemote() : true
NGPixel's avatar
NGPixel committed
230
  })
231 232 233 234 235 236
}).then(() => {
  return tasks.installDependencies()
}).then(() => {
  return tasks.ensureConfigFile()
}).then(() => {
  return tasks.startConfigurationWizard()
NGPixel's avatar
NGPixel committed
237 238 239
}).catch(err => {
  ora.fail(err)
})