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.

455 lines
13 KiB

  1. /* global wiki */
  2. const Promise = require('bluebird')
  3. const path = require('path')
  4. const fs = Promise.promisifyAll(require('fs-extra'))
  5. const _ = require('lodash')
  6. const entryHelper = require('../helpers/entry')
  7. /**
  8. * Documents Model
  9. */
  10. module.exports = {
  11. _repoPath: 'repo',
  12. _cachePath: 'data/cache',
  13. /**
  14. * Initialize Entries model
  15. *
  16. * @return {Object} Entries model instance
  17. */
  18. init() {
  19. let self = this
  20. self._repoPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo)
  21. self._cachePath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'cache')
  22. wiki.data.repoPath = self._repoPath
  23. wiki.data.cachePath = self._cachePath
  24. return self
  25. },
  26. /**
  27. * Check if a document already exists
  28. *
  29. * @param {String} entryPath The entry path
  30. * @return {Promise<Boolean>} True if exists, false otherwise
  31. */
  32. exists(entryPath) {
  33. let self = this
  34. return self.fetchOriginal(entryPath, {
  35. parseMarkdown: false,
  36. parseMeta: false,
  37. parseTree: false,
  38. includeMarkdown: false,
  39. includeParentInfo: false,
  40. cache: false
  41. }).then(() => {
  42. return true
  43. }).catch((err) => { // eslint-disable-line handle-callback-err
  44. return false
  45. })
  46. },
  47. /**
  48. * Fetch a document from cache, otherwise the original
  49. *
  50. * @param {String} entryPath The entry path
  51. * @return {Promise<Object>} Page Data
  52. */
  53. fetch(entryPath) {
  54. let self = this
  55. let cpath = entryHelper.getCachePath(entryPath)
  56. return fs.statAsync(cpath).then((st) => {
  57. return st.isFile()
  58. }).catch((err) => { // eslint-disable-line handle-callback-err
  59. return false
  60. }).then((isCache) => {
  61. if (isCache) {
  62. // Load from cache
  63. return fs.readFileAsync(cpath).then((contents) => {
  64. return JSON.parse(contents)
  65. }).catch((err) => { // eslint-disable-line handle-callback-err
  66. wiki.logger.error('Corrupted cache file. Deleting it...')
  67. fs.unlinkSync(cpath)
  68. return false
  69. })
  70. } else {
  71. // Load original
  72. return self.fetchOriginal(entryPath)
  73. }
  74. })
  75. },
  76. /**
  77. * Fetches the original document entry
  78. *
  79. * @param {String} entryPath The entry path
  80. * @param {Object} options The options
  81. * @return {Promise<Object>} Page data
  82. */
  83. fetchOriginal(entryPath, options) {
  84. let self = this
  85. let fpath = entryHelper.getFullPath(entryPath)
  86. let cpath = entryHelper.getCachePath(entryPath)
  87. options = _.defaults(options, {
  88. parseMarkdown: true,
  89. parseMeta: true,
  90. parseTree: true,
  91. includeMarkdown: false,
  92. includeParentInfo: true,
  93. cache: true
  94. })
  95. return fs.statAsync(fpath).then((st) => {
  96. if (st.isFile()) {
  97. return fs.readFileAsync(fpath, 'utf8').then((contents) => {
  98. let htmlProcessor = (options.parseMarkdown) ? wiki.mark.parseContent(contents) : Promise.resolve('')
  99. // Parse contents
  100. return htmlProcessor.then(html => {
  101. let pageData = {
  102. markdown: (options.includeMarkdown) ? contents : '',
  103. html,
  104. meta: (options.parseMeta) ? wiki.mark.parseMeta(contents) : {},
  105. tree: (options.parseTree) ? wiki.mark.parseTree(contents) : []
  106. }
  107. if (!pageData.meta.title) {
  108. pageData.meta.title = _.startCase(entryPath)
  109. }
  110. pageData.meta.path = entryPath
  111. // Get parent
  112. let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {
  113. return (pageData.parent = parentData)
  114. }).catch((err) => { // eslint-disable-line handle-callback-err
  115. return (pageData.parent = false)
  116. }) : Promise.resolve(true)
  117. return parentPromise.then(() => {
  118. // Cache to disk
  119. if (options.cache) {
  120. let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false)
  121. return fs.writeFileAsync(cpath, cacheData).catch((err) => {
  122. wiki.logger.error('Unable to write to cache! Performance may be affected.')
  123. wiki.logger.error(err)
  124. return true
  125. })
  126. } else {
  127. return true
  128. }
  129. }).return(pageData)
  130. })
  131. })
  132. } else {
  133. return false
  134. }
  135. }).catch((err) => { // eslint-disable-line handle-callback-err
  136. throw new Promise.OperationalError(wiki.lang.t('errors:notexist', { path: entryPath }))
  137. })
  138. },
  139. /**
  140. * Gets the parent information.
  141. *
  142. * @param {String} entryPath The entry path
  143. * @return {Promise<Object|False>} The parent information.
  144. */
  145. getParentInfo(entryPath) {
  146. if (_.includes(entryPath, '/')) {
  147. let parentParts = _.initial(_.split(entryPath, '/'))
  148. let parentPath = _.join(parentParts, '/')
  149. let parentFile = _.last(parentParts)
  150. let fpath = entryHelper.getFullPath(parentPath)
  151. return fs.statAsync(fpath).then((st) => {
  152. if (st.isFile()) {
  153. return fs.readFileAsync(fpath, 'utf8').then((contents) => {
  154. let pageMeta = wiki.mark.parseMeta(contents)
  155. return {
  156. path: parentPath,
  157. title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
  158. subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
  159. }
  160. })
  161. } else {
  162. return Promise.reject(new Error(wiki.lang.t('errors:parentinvalid')))
  163. }
  164. })
  165. } else {
  166. return Promise.reject(new Error(wiki.lang.t('errors:parentisroot')))
  167. }
  168. },
  169. /**
  170. * Update an existing document
  171. *
  172. * @param {String} entryPath The entry path
  173. * @param {String} contents The markdown-formatted contents
  174. * @param {Object} author The author user object
  175. * @return {Promise<Boolean>} True on success, false on failure
  176. */
  177. update(entryPath, contents, author) {
  178. let self = this
  179. let fpath = entryHelper.getFullPath(entryPath)
  180. return fs.statAsync(fpath).then((st) => {
  181. if (st.isFile()) {
  182. return self.makePersistent(entryPath, contents, author).then(() => {
  183. return self.updateCache(entryPath).then(entry => {
  184. return wiki.search.add(entry)
  185. })
  186. })
  187. } else {
  188. return Promise.reject(new Error(wiki.lang.t('errors:notexist', { path: entryPath })))
  189. }
  190. }).catch((err) => {
  191. wiki.logger.error(err)
  192. return Promise.reject(new Error(wiki.lang.t('errors:savefailed')))
  193. })
  194. },
  195. /**
  196. * Update local cache
  197. *
  198. * @param {String} entryPath The entry path
  199. * @return {Promise} Promise of the operation
  200. */
  201. updateCache(entryPath) {
  202. let self = this
  203. return self.fetchOriginal(entryPath, {
  204. parseMarkdown: true,
  205. parseMeta: true,
  206. parseTree: true,
  207. includeMarkdown: true,
  208. includeParentInfo: true,
  209. cache: true
  210. }).catch(err => {
  211. wiki.logger.error(err)
  212. return err
  213. }).then((pageData) => {
  214. return {
  215. entryPath,
  216. meta: pageData.meta,
  217. parent: pageData.parent || {},
  218. text: wiki.mark.removeMarkdown(pageData.markdown)
  219. }
  220. }).catch(err => {
  221. wiki.logger.error(err)
  222. return err
  223. }).then((content) => {
  224. let parentPath = _.chain(content.entryPath).split('/').initial().join('/').value()
  225. return wiki.db.Entry.findOneAndUpdate({
  226. _id: content.entryPath
  227. }, {
  228. _id: content.entryPath,
  229. title: content.meta.title || content.entryPath,
  230. subtitle: content.meta.subtitle || '',
  231. parentTitle: content.parent.title || '',
  232. parentPath: parentPath,
  233. isDirectory: false,
  234. isEntry: true
  235. }, {
  236. new: true,
  237. upsert: true
  238. }).then(result => {
  239. let plainResult = result.toObject()
  240. plainResult.text = content.text
  241. return plainResult
  242. })
  243. }).then(result => {
  244. return self.updateTreeInfo().then(() => {
  245. return result
  246. })
  247. }).catch(err => {
  248. wiki.logger.error(err)
  249. return err
  250. })
  251. },
  252. /**
  253. * Update tree info for all directory and parent entries
  254. *
  255. * @returns {Promise<Boolean>} Promise of the operation
  256. */
  257. updateTreeInfo() {
  258. return wiki.db.Entry.distinct('parentPath', { parentPath: { $ne: '' } }).then(allPaths => {
  259. if (allPaths.length > 0) {
  260. return Promise.map(allPaths, pathItem => {
  261. let parentPath = _.chain(pathItem).split('/').initial().join('/').value()
  262. let guessedTitle = _.chain(pathItem).split('/').last().startCase().value()
  263. return wiki.db.Entry.update({ _id: pathItem }, {
  264. $set: { isDirectory: true },
  265. $setOnInsert: { isEntry: false, title: guessedTitle, parentPath }
  266. }, { upsert: true })
  267. })
  268. } else {
  269. return true
  270. }
  271. })
  272. },
  273. /**
  274. * Create a new document
  275. *
  276. * @param {String} entryPath The entry path
  277. * @param {String} contents The markdown-formatted contents
  278. * @param {Object} author The author user object
  279. * @return {Promise<Boolean>} True on success, false on failure
  280. */
  281. create(entryPath, contents, author) {
  282. let self = this
  283. return self.exists(entryPath).then((docExists) => {
  284. if (!docExists) {
  285. return self.makePersistent(entryPath, contents, author).then(() => {
  286. return self.updateCache(entryPath).then(entry => {
  287. return wiki.search.add(entry)
  288. })
  289. })
  290. } else {
  291. return Promise.reject(new Error(wiki.lang.t('errors:alreadyexists')))
  292. }
  293. }).catch((err) => {
  294. wiki.logger.error(err)
  295. return Promise.reject(new Error(wiki.lang.t('errors:generic')))
  296. })
  297. },
  298. /**
  299. * Makes a document persistent to disk and git repository
  300. *
  301. * @param {String} entryPath The entry path
  302. * @param {String} contents The markdown-formatted contents
  303. * @param {Object} author The author user object
  304. * @return {Promise<Boolean>} True on success, false on failure
  305. */
  306. makePersistent(entryPath, contents, author) {
  307. let fpath = entryHelper.getFullPath(entryPath)
  308. return fs.outputFileAsync(fpath, contents).then(() => {
  309. return wiki.git.commitDocument(entryPath, author)
  310. })
  311. },
  312. /**
  313. * Move a document
  314. *
  315. * @param {String} entryPath The current entry path
  316. * @param {String} newEntryPath The new entry path
  317. * @param {Object} author The author user object
  318. * @return {Promise} Promise of the operation
  319. */
  320. move(entryPath, newEntryPath, author) {
  321. let self = this
  322. if (_.isEmpty(entryPath) || entryPath === 'home') {
  323. return Promise.reject(new Error(wiki.lang.t('errors:invalidpath')))
  324. }
  325. return wiki.git.moveDocument(entryPath, newEntryPath).then(() => {
  326. return wiki.git.commitDocument(newEntryPath, author).then(() => {
  327. // Delete old cache version
  328. let oldEntryCachePath = entryHelper.getCachePath(entryPath)
  329. fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err
  330. // Delete old index entry
  331. wiki.search.delete(entryPath)
  332. // Create cache for new entry
  333. return Promise.join(
  334. wiki.db.Entry.deleteOne({ _id: entryPath }),
  335. self.updateCache(newEntryPath).then(entry => {
  336. return wiki.search.add(entry)
  337. })
  338. )
  339. })
  340. })
  341. },
  342. /**
  343. * Delete a document
  344. *
  345. * @param {String} entryPath The current entry path
  346. * @param {Object} author The author user object
  347. * @return {Promise} Promise of the operation
  348. */
  349. remove(entryPath, author) {
  350. if (_.isEmpty(entryPath) || entryPath === 'home') {
  351. return Promise.reject(new Error(wiki.lang.t('errors:invalidpath')))
  352. }
  353. return wiki.git.deleteDocument(entryPath, author).then(() => {
  354. // Delete old cache version
  355. let oldEntryCachePath = entryHelper.getCachePath(entryPath)
  356. fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err
  357. // Delete old index entry
  358. wiki.search.delete(entryPath)
  359. // Delete entry
  360. return wiki.db.Entry.deleteOne({ _id: entryPath })
  361. })
  362. },
  363. /**
  364. * Generate a starter page content based on the entry path
  365. *
  366. * @param {String} entryPath The entry path
  367. * @return {Promise<String>} Starter content
  368. */
  369. getStarter(entryPath) {
  370. let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')))
  371. return fs.readFileAsync(path.join(wiki.SERVERPATH, 'app/content/create.md'), 'utf8').then((contents) => {
  372. return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle)
  373. })
  374. },
  375. /**
  376. * Get all entries from base path
  377. *
  378. * @param {String} basePath Path to list from
  379. * @param {Object} usr Current user
  380. * @return {Promise<Array>} List of entries
  381. */
  382. getFromTree(basePath, usr) {
  383. return wiki.db.Entry.find({ parentPath: basePath }, 'title parentPath isDirectory isEntry').sort({ title: 'asc' }).then(results => {
  384. return _.filter(results, r => {
  385. return wiki.rights.checkRole('/' + r._id, usr.rights, 'read')
  386. })
  387. })
  388. },
  389. getHistory(entryPath) {
  390. return wiki.db.Entry.findOne({ _id: entryPath, isEntry: true }).then(entry => {
  391. if (!entry) { return false }
  392. return wiki.git.getHistory(entryPath).then(history => {
  393. return {
  394. meta: entry,
  395. history
  396. }
  397. })
  398. })
  399. }
  400. }