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.

461 lines
18 KiB

  1. const _ = require('lodash')
  2. const cfgHelper = require('../helpers/config')
  3. const Promise = require('bluebird')
  4. const fs = require('fs-extra')
  5. const path = require('path')
  6. const zlib = require('zlib')
  7. const stream = require('stream')
  8. const pipeline = Promise.promisify(stream.pipeline)
  9. /* global WIKI */
  10. module.exports = {
  11. updates: {
  12. channel: 'BETA',
  13. version: WIKI.version,
  14. releaseDate: WIKI.releaseDate,
  15. minimumVersionRequired: '2.0.0-beta.0',
  16. minimumNodeRequired: '10.12.0'
  17. },
  18. exportStatus: {
  19. status: 'notrunning',
  20. progress: 0,
  21. message: '',
  22. updatedAt: null
  23. },
  24. init() {
  25. // Clear content cache
  26. fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
  27. return this
  28. },
  29. /**
  30. * Upgrade from WIKI.js 1.x - MongoDB database
  31. *
  32. * @param {Object} opts Options object
  33. */
  34. async upgradeFromMongo (opts) {
  35. WIKI.logger.info('Upgrading from MongoDB...')
  36. let mongo = require('mongodb').MongoClient
  37. let parsedMongoConStr = cfgHelper.parseConfigValue(opts.mongoCnStr)
  38. return new Promise((resolve, reject) => {
  39. // Connect to MongoDB
  40. mongo.connect(parsedMongoConStr, {
  41. autoReconnect: false,
  42. reconnectTries: 2,
  43. reconnectInterval: 1000,
  44. connectTimeoutMS: 5000,
  45. socketTimeoutMS: 5000
  46. }, async (err, db) => {
  47. try {
  48. if (err !== null) { throw err }
  49. let users = db.collection('users')
  50. // Check if users table is populated
  51. let userCount = await users.count()
  52. if (userCount < 2) {
  53. throw new Error('MongoDB Upgrade: Users table is empty!')
  54. }
  55. // Import all users
  56. let userData = await users.find({
  57. email: {
  58. $not: 'guest'
  59. }
  60. }).toArray()
  61. await WIKI.models.User.bulkCreate(_.map(userData, usr => {
  62. return {
  63. email: usr.email,
  64. name: usr.name || 'Imported User',
  65. password: usr.password || '',
  66. provider: usr.provider || 'local',
  67. providerId: usr.providerId || '',
  68. role: 'user',
  69. createdAt: usr.createdAt
  70. }
  71. }))
  72. resolve(true)
  73. } catch (errc) {
  74. reject(errc)
  75. }
  76. db.close()
  77. })
  78. })
  79. },
  80. /**
  81. * Export Wiki to Disk
  82. */
  83. async export (opts) {
  84. this.exportStatus.status = 'running'
  85. this.exportStatus.progress = 0
  86. this.exportStatus.message = ''
  87. this.exportStatus.startedAt = new Date()
  88. WIKI.logger.info(`Export started to path ${opts.path}`)
  89. WIKI.logger.info(`Entities to export: ${opts.entities.join(', ')}`)
  90. const progressMultiplier = 1 / opts.entities.length
  91. try {
  92. for (const entity of opts.entities) {
  93. switch (entity) {
  94. // -----------------------------------------
  95. // ASSETS
  96. // -----------------------------------------
  97. case 'assets': {
  98. WIKI.logger.info('Exporting assets...')
  99. const assetFolders = await WIKI.models.assetFolders.getAllPaths()
  100. const assetsCountRaw = await WIKI.models.assets.query().count('* as total').first()
  101. const assetsCount = parseInt(assetsCountRaw.total)
  102. if (assetsCount < 1) {
  103. WIKI.logger.warn('There are no assets to export! Skipping...')
  104. break
  105. }
  106. const assetsProgressMultiplier = progressMultiplier / Math.ceil(assetsCount / 50)
  107. WIKI.logger.info(`Found ${assetsCount} assets to export. Streaming to disk...`)
  108. await pipeline(
  109. WIKI.models.knex.select('filename', 'folderId', 'data').from('assets').join('assetData', 'assets.id', '=', 'assetData.id').stream(),
  110. new stream.Transform({
  111. objectMode: true,
  112. transform: async (asset, enc, cb) => {
  113. const filename = (asset.folderId && asset.folderId > 0) ? `${_.get(assetFolders, asset.folderId)}/${asset.filename}` : asset.filename
  114. WIKI.logger.info(`Exporting asset ${filename}...`)
  115. await fs.outputFile(path.join(opts.path, 'assets', filename), asset.data)
  116. this.exportStatus.progress += assetsProgressMultiplier * 100
  117. cb()
  118. }
  119. })
  120. )
  121. WIKI.logger.info('Export: assets saved to disk successfully.')
  122. break
  123. }
  124. // -----------------------------------------
  125. // COMMENTS
  126. // -----------------------------------------
  127. case 'comments': {
  128. WIKI.logger.info('Exporting comments...')
  129. const outputPath = path.join(opts.path, 'comments.json.gz')
  130. const commentsCountRaw = await WIKI.models.comments.query().count('* as total').first()
  131. const commentsCount = parseInt(commentsCountRaw.total)
  132. if (commentsCount < 1) {
  133. WIKI.logger.warn('There are no comments to export! Skipping...')
  134. break
  135. }
  136. const commentsProgressMultiplier = progressMultiplier / Math.ceil(commentsCount / 50)
  137. WIKI.logger.info(`Found ${commentsCount} comments to export. Streaming to file...`)
  138. const rs = stream.Readable({ objectMode: true })
  139. rs._read = () => {}
  140. const fetchCommentsBatch = async (offset) => {
  141. const comments = await WIKI.models.comments.query().offset(offset).limit(50).withGraphJoined({
  142. author: true,
  143. page: true
  144. }).modifyGraph('author', builder => {
  145. builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')
  146. }).modifyGraph('page', builder => {
  147. builder.select('pages.id', 'pages.path', 'pages.localeCode', 'pages.title')
  148. })
  149. if (comments.length > 0) {
  150. for (const cmt of comments) {
  151. rs.push(cmt)
  152. }
  153. fetchCommentsBatch(offset + 50)
  154. } else {
  155. rs.push(null)
  156. }
  157. this.exportStatus.progress += commentsProgressMultiplier * 100
  158. }
  159. fetchCommentsBatch(0)
  160. let marker = 0
  161. await pipeline(
  162. rs,
  163. new stream.Transform({
  164. objectMode: true,
  165. transform (chunk, encoding, callback) {
  166. marker++
  167. let outputStr = marker === 1 ? '[\n' : ''
  168. outputStr += JSON.stringify(chunk, null, 2)
  169. if (marker < commentsCount) {
  170. outputStr += ',\n'
  171. }
  172. callback(null, outputStr)
  173. },
  174. flush (callback) {
  175. callback(null, '\n]')
  176. }
  177. }),
  178. zlib.createGzip(),
  179. fs.createWriteStream(outputPath)
  180. )
  181. WIKI.logger.info('Export: comments.json.gz created successfully.')
  182. break
  183. }
  184. // -----------------------------------------
  185. // GROUPS
  186. // -----------------------------------------
  187. case 'groups': {
  188. WIKI.logger.info('Exporting groups...')
  189. const outputPath = path.join(opts.path, 'groups.json')
  190. const groups = await WIKI.models.groups.query()
  191. await fs.outputJSON(outputPath, groups, { spaces: 2 })
  192. WIKI.logger.info('Export: groups.json created successfully.')
  193. this.exportStatus.progress += progressMultiplier * 100
  194. break
  195. }
  196. // -----------------------------------------
  197. // HISTORY
  198. // -----------------------------------------
  199. case 'history': {
  200. WIKI.logger.info('Exporting pages history...')
  201. const outputPath = path.join(opts.path, 'pages-history.json.gz')
  202. const pagesCountRaw = await WIKI.models.pageHistory.query().count('* as total').first()
  203. const pagesCount = parseInt(pagesCountRaw.total)
  204. if (pagesCount < 1) {
  205. WIKI.logger.warn('There are no pages history to export! Skipping...')
  206. break
  207. }
  208. const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10)
  209. WIKI.logger.info(`Found ${pagesCount} pages history to export. Streaming to file...`)
  210. const rs = stream.Readable({ objectMode: true })
  211. rs._read = () => {}
  212. const fetchPagesBatch = async (offset) => {
  213. const pages = await WIKI.models.pageHistory.query().offset(offset).limit(10).withGraphJoined({
  214. author: true,
  215. page: true,
  216. tags: true
  217. }).modifyGraph('author', builder => {
  218. builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')
  219. }).modifyGraph('page', builder => {
  220. builder.select('pages.id', 'pages.title', 'pages.path', 'pages.localeCode')
  221. }).modifyGraph('tags', builder => {
  222. builder.select('tags.tag', 'tags.title')
  223. })
  224. if (pages.length > 0) {
  225. for (const page of pages) {
  226. rs.push(page)
  227. }
  228. fetchPagesBatch(offset + 10)
  229. } else {
  230. rs.push(null)
  231. }
  232. this.exportStatus.progress += pagesProgressMultiplier * 100
  233. }
  234. fetchPagesBatch(0)
  235. let marker = 0
  236. await pipeline(
  237. rs,
  238. new stream.Transform({
  239. objectMode: true,
  240. transform (chunk, encoding, callback) {
  241. marker++
  242. let outputStr = marker === 1 ? '[\n' : ''
  243. outputStr += JSON.stringify(chunk, null, 2)
  244. if (marker < pagesCount) {
  245. outputStr += ',\n'
  246. }
  247. callback(null, outputStr)
  248. },
  249. flush (callback) {
  250. callback(null, '\n]')
  251. }
  252. }),
  253. zlib.createGzip(),
  254. fs.createWriteStream(outputPath)
  255. )
  256. WIKI.logger.info('Export: pages-history.json.gz created successfully.')
  257. break
  258. }
  259. // -----------------------------------------
  260. // NAVIGATION
  261. // -----------------------------------------
  262. case 'navigation': {
  263. WIKI.logger.info('Exporting navigation...')
  264. const outputPath = path.join(opts.path, 'navigation.json')
  265. const navigationRaw = await WIKI.models.navigation.query()
  266. const navigation = navigationRaw.reduce((obj, cur) => {
  267. obj[cur.key] = cur.config
  268. return obj
  269. }, {})
  270. await fs.outputJSON(outputPath, navigation, { spaces: 2 })
  271. WIKI.logger.info('Export: navigation.json created successfully.')
  272. this.exportStatus.progress += progressMultiplier * 100
  273. break
  274. }
  275. // -----------------------------------------
  276. // PAGES
  277. // -----------------------------------------
  278. case 'pages': {
  279. WIKI.logger.info('Exporting pages...')
  280. const outputPath = path.join(opts.path, 'pages.json.gz')
  281. const pagesCountRaw = await WIKI.models.pages.query().count('* as total').first()
  282. const pagesCount = parseInt(pagesCountRaw.total)
  283. if (pagesCount < 1) {
  284. WIKI.logger.warn('There are no pages to export! Skipping...')
  285. break
  286. }
  287. const pagesProgressMultiplier = progressMultiplier / Math.ceil(pagesCount / 10)
  288. WIKI.logger.info(`Found ${pagesCount} pages to export. Streaming to file...`)
  289. const rs = stream.Readable({ objectMode: true })
  290. rs._read = () => {}
  291. const fetchPagesBatch = async (offset) => {
  292. const pages = await WIKI.models.pages.query().offset(offset).limit(10).withGraphJoined({
  293. author: true,
  294. creator: true,
  295. tags: true
  296. }).modifyGraph('author', builder => {
  297. builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')
  298. }).modifyGraph('creator', builder => {
  299. builder.select('users.id', 'users.name', 'users.email', 'users.providerKey')
  300. }).modifyGraph('tags', builder => {
  301. builder.select('tags.tag', 'tags.title')
  302. })
  303. if (pages.length > 0) {
  304. for (const page of pages) {
  305. rs.push(page)
  306. }
  307. fetchPagesBatch(offset + 10)
  308. } else {
  309. rs.push(null)
  310. }
  311. this.exportStatus.progress += pagesProgressMultiplier * 100
  312. }
  313. fetchPagesBatch(0)
  314. let marker = 0
  315. await pipeline(
  316. rs,
  317. new stream.Transform({
  318. objectMode: true,
  319. transform (chunk, encoding, callback) {
  320. marker++
  321. let outputStr = marker === 1 ? '[\n' : ''
  322. outputStr += JSON.stringify(chunk, null, 2)
  323. if (marker < pagesCount) {
  324. outputStr += ',\n'
  325. }
  326. callback(null, outputStr)
  327. },
  328. flush (callback) {
  329. callback(null, '\n]')
  330. }
  331. }),
  332. zlib.createGzip(),
  333. fs.createWriteStream(outputPath)
  334. )
  335. WIKI.logger.info('Export: pages.json.gz created successfully.')
  336. break
  337. }
  338. // -----------------------------------------
  339. // SETTINGS
  340. // -----------------------------------------
  341. case 'settings': {
  342. WIKI.logger.info('Exporting settings...')
  343. const outputPath = path.join(opts.path, 'settings.json')
  344. const config = {
  345. ...WIKI.config,
  346. modules: {
  347. analytics: await WIKI.models.analytics.query(),
  348. authentication: (await WIKI.models.authentication.query()).map(a => ({
  349. ...a,
  350. domainWhitelist: _.get(a, 'domainWhitelist.v', []),
  351. autoEnrollGroups: _.get(a, 'autoEnrollGroups.v', [])
  352. })),
  353. commentProviders: await WIKI.models.commentProviders.query(),
  354. renderers: await WIKI.models.renderers.query(),
  355. searchEngines: await WIKI.models.searchEngines.query(),
  356. storage: await WIKI.models.storage.query()
  357. },
  358. apiKeys: await WIKI.models.apiKeys.query().where('isRevoked', false)
  359. }
  360. await fs.outputJSON(outputPath, config, { spaces: 2 })
  361. WIKI.logger.info('Export: settings.json created successfully.')
  362. this.exportStatus.progress += progressMultiplier * 100
  363. break
  364. }
  365. // -----------------------------------------
  366. // USERS
  367. // -----------------------------------------
  368. case 'users': {
  369. WIKI.logger.info('Exporting users...')
  370. const outputPath = path.join(opts.path, 'users.json.gz')
  371. const usersCountRaw = await WIKI.models.users.query().count('* as total').first()
  372. const usersCount = parseInt(usersCountRaw.total)
  373. if (usersCount < 1) {
  374. WIKI.logger.warn('There are no users to export! Skipping...')
  375. break
  376. }
  377. const usersProgressMultiplier = progressMultiplier / Math.ceil(usersCount / 50)
  378. WIKI.logger.info(`Found ${usersCount} users to export. Streaming to file...`)
  379. const rs = stream.Readable({ objectMode: true })
  380. rs._read = () => {}
  381. const fetchUsersBatch = async (offset) => {
  382. const users = await WIKI.models.users.query().offset(offset).limit(50).withGraphJoined({
  383. groups: true,
  384. provider: true
  385. }).modifyGraph('groups', builder => {
  386. builder.select('groups.id', 'groups.name')
  387. }).modifyGraph('provider', builder => {
  388. builder.select('authentication.key', 'authentication.strategyKey', 'authentication.displayName')
  389. })
  390. if (users.length > 0) {
  391. for (const usr of users) {
  392. rs.push(usr)
  393. }
  394. fetchUsersBatch(offset + 50)
  395. } else {
  396. rs.push(null)
  397. }
  398. this.exportStatus.progress += usersProgressMultiplier * 100
  399. }
  400. fetchUsersBatch(0)
  401. let marker = 0
  402. await pipeline(
  403. rs,
  404. new stream.Transform({
  405. objectMode: true,
  406. transform (chunk, encoding, callback) {
  407. marker++
  408. let outputStr = marker === 1 ? '[\n' : ''
  409. outputStr += JSON.stringify(chunk, null, 2)
  410. if (marker < usersCount) {
  411. outputStr += ',\n'
  412. }
  413. callback(null, outputStr)
  414. },
  415. flush (callback) {
  416. callback(null, '\n]')
  417. }
  418. }),
  419. zlib.createGzip(),
  420. fs.createWriteStream(outputPath)
  421. )
  422. WIKI.logger.info('Export: users.json.gz created successfully.')
  423. break
  424. }
  425. }
  426. }
  427. this.exportStatus.status = 'success'
  428. this.exportStatus.progress = 100
  429. } catch (err) {
  430. this.exportStatus.status = 'error'
  431. this.exportStatus.message = err.message
  432. }
  433. }
  434. }