agent.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  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, {
  94. filter: pathItem => {
  95. return !pathItem.endsWith('.git')
  96. }
  97. }).on('data', function (item) {
  98. if (path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') {
  99. let entryPath = entryHelper.parsePath(entryHelper.getEntryPathFromFullPath(item.path))
  100. let cachePath = entryHelper.getCachePath(entryPath)
  101. // -> Purge outdated cache
  102. cacheJobs.push(
  103. fs.statAsync(cachePath).then((st) => {
  104. return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active'
  105. }).catch((err) => {
  106. return (err.code !== 'EEXIST') ? err : 'new'
  107. }).then((fileStatus) => {
  108. // -> Delete expired cache file
  109. if (fileStatus === 'expired') {
  110. return fs.unlinkAsync(cachePath).return(fileStatus)
  111. }
  112. return fileStatus
  113. }).then((fileStatus) => {
  114. // -> Update cache and search index
  115. if (fileStatus !== 'active') {
  116. return global.entries.updateCache(entryPath).then(entry => {
  117. process.send({
  118. action: 'searchAdd',
  119. content: entry
  120. })
  121. return true
  122. })
  123. }
  124. return true
  125. })
  126. )
  127. }
  128. }).on('error', (err, item) => {
  129. global.winston.error(err)
  130. }).on('end', () => {
  131. jobCbStreamDocsResolve(Promise.all(cacheJobs))
  132. })
  133. return jobCbStreamDocs
  134. }))
  135. //* ****************************************
  136. // -> Clear failed temporary upload files
  137. //* ****************************************
  138. jobs.push(
  139. fs.readdirAsync(uploadsTempPath).then((ls) => {
  140. let fifteenAgo = moment().subtract(15, 'minutes')
  141. return Promise.map(ls, (f) => {
  142. return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s } })
  143. }).filter((s) => { return s.stat.isFile() }).then((arrFiles) => {
  144. return Promise.map(arrFiles, (f) => {
  145. if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
  146. return fs.unlinkAsync(path.join(uploadsTempPath, f.filename))
  147. } else {
  148. return true
  149. }
  150. })
  151. })
  152. })
  153. )
  154. // ----------------------------------------
  155. // Run
  156. // ----------------------------------------
  157. Promise.all(jobs).then(() => {
  158. global.winston.info('All jobs completed successfully! Going to sleep for now.')
  159. if (!jobUplWatchStarted) {
  160. jobUplWatchStarted = true
  161. global.upl.initialScan().then(() => {
  162. job.start()
  163. })
  164. }
  165. return true
  166. }).catch((err) => {
  167. global.winston.error('One or more jobs have failed: ', err)
  168. }).finally(() => {
  169. jobIsBusy = false
  170. })
  171. },
  172. start: false,
  173. timeZone: 'UTC',
  174. runOnInit: true
  175. })
  176. })
  177. // ----------------------------------------
  178. // Shutdown gracefully
  179. // ----------------------------------------
  180. process.on('disconnect', () => {
  181. global.winston.warn('Lost connection to main server. Exiting...')
  182. job.stop()
  183. process.exit()
  184. })
  185. process.on('exit', () => {
  186. job.stop()
  187. })