You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

278 lines
10 KiB

  1. 'use strict'
  2. // =====================================================
  3. // Wiki.js
  4. // Installation Script
  5. // =====================================================
  6. const Promise = require('bluebird')
  7. const _ = require('lodash')
  8. const colors = require('colors/safe')
  9. const exec = require('execa')
  10. const fs = Promise.promisifyAll(require('fs-extra'))
  11. const https = require('follow-redirects').https
  12. const inquirer = require('inquirer')
  13. const os = require('os')
  14. const path = require('path')
  15. const pm2 = Promise.promisifyAll(require('pm2'))
  16. const tar = require('tar')
  17. const zlib = require('zlib')
  18. const installDir = path.resolve(__dirname, '../..')
  19. const isContainerBased = (process.env.WIKI_JS_HEROKU || process.env.WIKI_JS_DOCKER)
  20. let installMode = 'new'
  21. // =====================================================
  22. // INSTALLATION TASKS
  23. // =====================================================
  24. const tasks = {
  25. /**
  26. * Stop and delete existing instances
  27. */
  28. stopAndDeleteInstances() {
  29. ora.text = 'Looking for running instances...'
  30. return pm2.connectAsync().then(() => {
  31. return pm2.describeAsync('wiki').then(() => {
  32. ora.text = 'Stopping and deleting process from pm2...'
  33. return pm2.deleteAsync('wiki')
  34. }).catch(err => { // eslint-disable-line handle-callback-err
  35. return true
  36. }).finally(() => {
  37. pm2.disconnect()
  38. })
  39. }).catch(err => { // eslint-disable-line handle-callback-err
  40. return true
  41. })
  42. },
  43. /**
  44. * Check for sufficient memory
  45. */
  46. checkRequirements() {
  47. ora.text = 'Checking system requirements...'
  48. if (os.totalmem() < 1000 * 1000 * 768) {
  49. return Promise.reject(new Error('Not enough memory to install dependencies. Minimum is 768 MB.'))
  50. } else {
  51. return Promise.resolve(true)
  52. }
  53. },
  54. /**
  55. * Install via local tarball if present
  56. */
  57. installFromLocal() {
  58. let hasTarball = true
  59. let tbPath = path.join(installDir, 'wiki-js.tar.gz')
  60. return fs.accessAsync(tbPath)
  61. .catch(err => { // eslint-disable-line handle-callback-err
  62. hasTarball = false
  63. }).then(() => {
  64. if (hasTarball) {
  65. ora.text = 'Local tarball found. Extracting...'
  66. return new Promise((resolve, reject) => {
  67. fs.createReadStream(tbPath).pipe(zlib.createGunzip())
  68. .pipe(tar.extract({ cwd: installDir }))
  69. .on('error', err => reject(err))
  70. .on('end', () => {
  71. ora.text = 'Tarball extracted successfully.'
  72. resolve(tasks.installDependencies())
  73. isContainerBased && console.info('>> Installing dependencies...')
  74. })
  75. })
  76. } else {
  77. return false
  78. }
  79. })
  80. },
  81. /**
  82. * Install from GitHub release download
  83. */
  84. installFromRemote() {
  85. // Fetch version from npm package
  86. return fs.readJsonAsync('package.json').then((packageObj) => {
  87. let versionGet = _.chain(packageObj.version).split('.').take(4).join('.')
  88. let remoteURLApp = _.replace('https://github.com/Requarks/wiki/releases/download/v{0}/wiki-js.tar.gz', '{0}', versionGet)
  89. let remoteURLDeps = _.replace('https://github.com/Requarks/wiki/releases/download/v{0}/node_modules.tar.gz', '{0}', versionGet)
  90. return new Promise((resolve, reject) => {
  91. // Fetch app tarball
  92. ora.text = 'Looking for app package...'
  93. https.get(remoteURLApp, resp => {
  94. if (resp.statusCode !== 200) {
  95. return reject(new Error('Remote file not found'))
  96. }
  97. ora.text = 'Remote app tarball found. Downloading...'
  98. isContainerBased && console.info('>> Extracting app to ' + installDir)
  99. // Extract app tarball
  100. resp.pipe(zlib.createGunzip())
  101. .pipe(tar.extract({ cwd: installDir }))
  102. .on('error', err => reject(err))
  103. .on('end', () => {
  104. ora.text = 'App tarball extracted successfully.'
  105. resolve(true)
  106. })
  107. })
  108. }).then(() => {
  109. return new Promise((resolve, reject) => {
  110. // Fetch deps tarball
  111. ora.text = 'Looking for dependencies package...'
  112. https.get(remoteURLDeps, resp => {
  113. if (resp.statusCode !== 200) {
  114. return reject(new Error('Remote file not found'))
  115. }
  116. ora.text = 'Remote dependencies tarball found. Downloading...'
  117. isContainerBased && console.info('>> Extracting dependencies to ' + installDir)
  118. // Extract deps tarball
  119. resp.pipe(zlib.createGunzip())
  120. .pipe(tar.extract({ cwd: path.join(installDir, 'node_modules') }))
  121. .on('error', err => reject(err))
  122. .on('end', () => {
  123. ora.text = 'Dependencies tarball extracted successfully.'
  124. resolve(true)
  125. })
  126. })
  127. })
  128. })
  129. })
  130. },
  131. /**
  132. * Create default config.yml file if new installation
  133. */
  134. ensureConfigFile() {
  135. return fs.accessAsync(path.join(installDir, 'config.yml')).then(() => {
  136. // Is Upgrade
  137. ora.text = 'Existing config.yml found. Upgrade mode.'
  138. installMode = 'upgrade'
  139. return true
  140. }).catch(err => {
  141. // Is New Install
  142. if (err.code === 'ENOENT') {
  143. ora.text = 'First-time install, creating a new config.yml...'
  144. installMode = 'new'
  145. let sourceConfigFile = path.join(installDir, 'config.sample.yml')
  146. if (process.env.WIKI_JS_HEROKU || process.env.WIKI_JS_DOCKER) {
  147. sourceConfigFile = path.join(__dirname, 'configs/config.passive.yml')
  148. }
  149. return fs.copyAsync(sourceConfigFile, path.join(installDir, 'config.yml'))
  150. } else {
  151. return err
  152. }
  153. })
  154. },
  155. /**
  156. * Install npm dependencies
  157. */
  158. installDependencies() {
  159. ora.text = 'Installing Wiki.js npm dependencies...'
  160. return exec.stdout('npm', ['install', '--only=production', '--no-optional'], {
  161. cwd: installDir
  162. }).then(results => {
  163. ora.text = 'Wiki.js npm dependencies installed successfully.'
  164. return true
  165. })
  166. },
  167. startConfigurationWizard() {
  168. ora.succeed('Installation succeeded.')
  169. if (process.env.WIKI_JS_HEROKU) {
  170. console.info('> Wiki.js has been installed and is configured to use Heroku configuration.')
  171. return true
  172. } else if (process.env.WIKI_JS_DOCKER) {
  173. console.info('Docker environment detected. Skipping setup wizard auto-start.')
  174. return true
  175. } else if (process.stdout.isTTY) {
  176. if (installMode === 'upgrade') {
  177. console.info(colors.yellow('\n!!! IMPORTANT !!!'))
  178. 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.'))
  179. 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'))
  180. }
  181. inquirer.prompt([{
  182. type: 'list',
  183. name: 'action',
  184. message: 'Continue with configuration wizard?',
  185. default: 'default',
  186. choices: [
  187. { name: 'Yes, run configuration wizard on port 3000 (recommended)', value: 'default', short: 'Yes' },
  188. { name: 'Yes, run configuration wizard on a custom port...', value: 'custom', short: 'Yes' },
  189. { name: 'No, I\'ll configure the config file manually', value: 'exit', short: 'No' }
  190. ]
  191. }, {
  192. type: 'input',
  193. name: 'customport',
  194. message: 'Custom port to use:',
  195. default: 3000,
  196. validate: (val) => {
  197. val = _.toNumber(val)
  198. return (_.isNumber(val) && val > 0) ? true : 'Invalid Port!'
  199. },
  200. when: (ans) => {
  201. return ans.action === 'custom'
  202. }
  203. }]).then((ans) => {
  204. switch (ans.action) {
  205. case 'default':
  206. 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!)'))
  207. ora = require('ora')({ text: 'I\'ll wait until you\'re done ;)', color: 'yellow', spinner: 'pong' }).start()
  208. return exec.stdout('node', ['wiki', 'configure'], {
  209. cwd: installDir
  210. })
  211. case 'custom':
  212. 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!)'))
  213. ora = require('ora')({ text: 'I\'ll wait until you\'re done ;)', color: 'yellow', spinner: 'pong' }).start()
  214. return exec.stdout('node', ['wiki', 'configure', ans.customport], {
  215. cwd: installDir
  216. })
  217. default:
  218. 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'))
  219. return Promise.delay(7000).then(() => {
  220. process.exit(0)
  221. })
  222. }
  223. }).then(() => {
  224. ora.succeed(colors.bold.green('Wiki.js has been configured successfully. It is now starting up and should be accessible very soon!'))
  225. return Promise.delay(3000).then(() => {
  226. console.info('npm is finishing... please wait...')
  227. })
  228. })
  229. } else {
  230. console.info(colors.cyan('[WARNING] Non-interactive terminal detected. You must manually start the configuration wizard using the command: node wiki configure'))
  231. }
  232. }
  233. }
  234. // =====================================================
  235. // INSTALL SEQUENCE
  236. // =====================================================
  237. if (!isContainerBased) {
  238. console.info(colors.yellow(
  239. ' __ __ _ _ _ _ \n' +
  240. '/ / /\\ \\ (_) | _(_) (_)___ \n' +
  241. '\\ \\/ \\/ / | |/ / | | / __| \n' +
  242. ' \\ /\\ /| | <| |_ | \\__ \\ \n' +
  243. ' \\/ \\/ |_|_|\\_\\_(_)/ |___/ \n' +
  244. ' |__/\n'))
  245. } else {
  246. console.info('> WIKI.JS [Installing...]')
  247. }
  248. let ora = require('ora')({ text: 'Initializing...', spinner: 'dots12' }).start()
  249. Promise.join(
  250. tasks.stopAndDeleteInstances(),
  251. tasks.checkRequirements()
  252. ).then(() => {
  253. isContainerBased && console.info('>> Fetching tarball...')
  254. return tasks.installFromLocal().then(succeeded => {
  255. return (!succeeded) ? tasks.installFromRemote() : true
  256. })
  257. }).then(() => {
  258. isContainerBased && console.info('>> Creating config file...')
  259. return tasks.ensureConfigFile()
  260. }).then(() => {
  261. return tasks.startConfigurationWizard()
  262. }).catch(err => {
  263. isContainerBased && console.error(err)
  264. ora.fail(err)
  265. })