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.

222 lines
6.5 KiB

  1. // ===========================================
  2. // Wiki.js - Background Agent
  3. // 1.0.0
  4. // Licensed under AGPLv3
  5. // ===========================================
  6. const path = require('path')
  7. const ROOTPATH = process.cwd()
  8. const SERVERPATH = path.join(ROOTPATH, 'server')
  9. global.ROOTPATH = ROOTPATH
  10. global.SERVERPATH = SERVERPATH
  11. const IS_DEBUG = process.env.NODE_ENV === 'development'
  12. let appconf = require('./libs/config')()
  13. global.appconfig = appconf.config
  14. global.appdata = appconf.data
  15. // ----------------------------------------
  16. // Load Winston
  17. // ----------------------------------------
  18. global.winston = require('./libs/logger')(IS_DEBUG, 'AGENT')
  19. // ----------------------------------------
  20. // Load global modules
  21. // ----------------------------------------
  22. global.winston.info('Background Agent is initializing...')
  23. global.db = require('./libs/db').init()
  24. global.upl = require('./libs/uploads-agent').init()
  25. global.git = require('./libs/git').init()
  26. global.entries = require('./libs/entries').init()
  27. global.lang = require('i18next')
  28. global.mark = require('./libs/markdown')
  29. // ----------------------------------------
  30. // Load modules
  31. // ----------------------------------------
  32. const moment = require('moment')
  33. const Promise = require('bluebird')
  34. const fs = Promise.promisifyAll(require('fs-extra'))
  35. const klaw = require('klaw')
  36. const Cron = require('cron').CronJob
  37. const i18nBackend = require('i18next-node-fs-backend')
  38. const entryHelper = require('./helpers/entry')
  39. // ----------------------------------------
  40. // Localization Engine
  41. // ----------------------------------------
  42. global.lang
  43. .use(i18nBackend)
  44. .init({
  45. load: 'languageOnly',
  46. ns: ['common', 'admin', 'auth', 'errors', 'git'],
  47. defaultNS: 'common',
  48. saveMissing: false,
  49. preload: [appconfig.lang],
  50. lng: appconfig.lang,
  51. fallbackLng: 'en',
  52. backend: {
  53. loadPath: path.join(SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
  54. }
  55. })
  56. // ----------------------------------------
  57. // Start Cron
  58. // ----------------------------------------
  59. let job
  60. let jobIsBusy = false
  61. let jobUplWatchStarted = false
  62. global.db.onReady.then(() => {
  63. return global.db.Entry.remove({})
  64. }).then(() => {
  65. job = new Cron({
  66. cronTime: '0 */5 * * * *',
  67. onTick: () => {
  68. // Make sure we don't start two concurrent jobs
  69. if (jobIsBusy) {
  70. global.winston.warn('Previous job has not completed gracefully or is still running! Skipping for now. (This is not normal, you should investigate)')
  71. return
  72. }
  73. global.winston.info('Running all jobs...')
  74. jobIsBusy = true
  75. // Prepare async job collector
  76. let jobs = []
  77. let repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
  78. let dataPath = path.resolve(ROOTPATH, appconfig.paths.data)
  79. let uploadsTempPath = path.join(dataPath, 'temp-upload')
  80. // ----------------------------------------
  81. // REGULAR JOBS
  82. // ----------------------------------------
  83. //* ****************************************
  84. // -> Sync with Git remote
  85. //* ****************************************
  86. jobs.push(global.git.resync().then(() => {
  87. // -> Stream all documents
  88. let cacheJobs = []
  89. let jobCbStreamDocsResolve = null
  90. let jobCbStreamDocs = new Promise((resolve, reject) => {
  91. jobCbStreamDocsResolve = resolve
  92. })
  93. klaw(repoPath).on('data', function (item) {
  94. if (path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') {
  95. let entryPath = entryHelper.parsePath(entryHelper.getEntryPathFromFullPath(item.path))
  96. let cachePath = entryHelper.getCachePath(entryPath)
  97. // -> Purge outdated cache
  98. cacheJobs.push(
  99. fs.statAsync(cachePath).then((st) => {
  100. return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active'
  101. }).catch((err) => {
  102. return (err.code !== 'EEXIST') ? err : 'new'
  103. }).then((fileStatus) => {
  104. // -> Delete expired cache file
  105. if (fileStatus === 'expired') {
  106. return fs.unlinkAsync(cachePath).return(fileStatus)
  107. }
  108. return fileStatus
  109. }).then((fileStatus) => {
  110. // -> Update cache and search index
  111. if (fileStatus !== 'active') {
  112. return global.entries.updateCache(entryPath).then(entry => {
  113. process.send({
  114. action: 'searchAdd',
  115. content: entry
  116. })
  117. return true
  118. })
  119. }
  120. return true
  121. })
  122. )
  123. }
  124. }).on('end', () => {
  125. jobCbStreamDocsResolve(Promise.all(cacheJobs))
  126. })
  127. return jobCbStreamDocs
  128. }))
  129. //* ****************************************
  130. // -> Clear failed temporary upload files
  131. //* ****************************************
  132. jobs.push(
  133. fs.readdirAsync(uploadsTempPath).then((ls) => {
  134. let fifteenAgo = moment().subtract(15, 'minutes')
  135. return Promise.map(ls, (f) => {
  136. return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s } })
  137. }).filter((s) => { return s.stat.isFile() }).then((arrFiles) => {
  138. return Promise.map(arrFiles, (f) => {
  139. if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
  140. return fs.unlinkAsync(path.join(uploadsTempPath, f.filename))
  141. } else {
  142. return true
  143. }
  144. })
  145. })
  146. })
  147. )
  148. // ----------------------------------------
  149. // Run
  150. // ----------------------------------------
  151. Promise.all(jobs).then(() => {
  152. global.winston.info('All jobs completed successfully! Going to sleep for now.')
  153. if (!jobUplWatchStarted) {
  154. jobUplWatchStarted = true
  155. global.upl.initialScan().then(() => {
  156. job.start()
  157. })
  158. }
  159. return true
  160. }).catch((err) => {
  161. global.winston.error('One or more jobs have failed: ', err)
  162. }).finally(() => {
  163. jobIsBusy = false
  164. })
  165. },
  166. start: false,
  167. timeZone: 'UTC',
  168. runOnInit: true
  169. })
  170. })
  171. // ----------------------------------------
  172. // Shutdown gracefully
  173. // ----------------------------------------
  174. process.on('disconnect', () => {
  175. global.winston.warn('Lost connection to main server. Exiting...')
  176. job.stop()
  177. process.exit()
  178. })
  179. process.on('exit', () => {
  180. job.stop()
  181. })