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.

421 lines
12 KiB

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