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.

397 lines
9.2 KiB

  1. "use strict";
  2. var Promise = require('bluebird'),
  3. path = require('path'),
  4. fs = Promise.promisifyAll(require("fs-extra")),
  5. _ = require('lodash'),
  6. farmhash = require('farmhash'),
  7. BSONModule = require('bson'),
  8. BSON = new BSONModule.BSONPure.BSON(),
  9. moment = require('moment');
  10. /**
  11. * Entries Model
  12. */
  13. module.exports = {
  14. _repoPath: 'repo',
  15. _cachePath: 'data/cache',
  16. /**
  17. * Initialize Entries model
  18. *
  19. * @param {Object} appconfig The application config
  20. * @return {Object} Entries model instance
  21. */
  22. init(appconfig) {
  23. let self = this;
  24. self._repoPath = path.resolve(ROOTPATH, appconfig.datadir.repo);
  25. self._cachePath = path.resolve(ROOTPATH, appconfig.datadir.db, 'cache');
  26. return self;
  27. },
  28. /**
  29. * Check if a document already exists
  30. *
  31. * @param {String} entryPath The entry path
  32. * @return {Promise<Boolean>} True if exists, false otherwise
  33. */
  34. exists(entryPath) {
  35. let self = this;
  36. return self.fetchOriginal(entryPath, {
  37. parseMarkdown: false,
  38. parseMeta: false,
  39. parseTree: false,
  40. includeMarkdown: false,
  41. includeParentInfo: false,
  42. cache: false
  43. }).then(() => {
  44. return true;
  45. }).catch((err) => {
  46. return false;
  47. });
  48. },
  49. /**
  50. * Fetch a document from cache, otherwise the original
  51. *
  52. * @param {String} entryPath The entry path
  53. * @return {Promise<Object>} Page Data
  54. */
  55. fetch(entryPath) {
  56. let self = this;
  57. let cpath = self.getCachePath(entryPath);
  58. return fs.statAsync(cpath).then((st) => {
  59. return st.isFile();
  60. }).catch((err) => {
  61. return false;
  62. }).then((isCache) => {
  63. if(isCache) {
  64. // Load from cache
  65. return fs.readFileAsync(cpath).then((contents) => {
  66. return BSON.deserialize(contents);
  67. }).catch((err) => {
  68. winston.error('Corrupted cache file. Deleting it...');
  69. fs.unlinkSync(cpath);
  70. return false;
  71. });
  72. } else {
  73. // Load original
  74. return self.fetchOriginal(entryPath);
  75. }
  76. });
  77. },
  78. /**
  79. * Fetches the original document entry
  80. *
  81. * @param {String} entryPath The entry path
  82. * @param {Object} options The options
  83. * @return {Promise<Object>} Page data
  84. */
  85. fetchOriginal(entryPath, options) {
  86. let self = this;
  87. let fpath = self.getFullPath(entryPath);
  88. let cpath = self.getCachePath(entryPath);
  89. options = _.defaults(options, {
  90. parseMarkdown: true,
  91. parseMeta: true,
  92. parseTree: true,
  93. includeMarkdown: false,
  94. includeParentInfo: true,
  95. cache: true
  96. });
  97. return fs.statAsync(fpath).then((st) => {
  98. if(st.isFile()) {
  99. return fs.readFileAsync(fpath, 'utf8').then((contents) => {
  100. // Parse contents
  101. let pageData = {
  102. markdown: (options.includeMarkdown) ? contents : '',
  103. html: (options.parseMarkdown) ? mark.parseContent(contents) : '',
  104. meta: (options.parseMeta) ? mark.parseMeta(contents) : {},
  105. tree: (options.parseTree) ? 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) => {
  115. return (pageData.parent = false);
  116. }) : Promise.resolve(true);
  117. return parentPromise.then(() => {
  118. // Cache to disk
  119. if(options.cache) {
  120. let cacheData = BSON.serialize(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false);
  121. return fs.writeFileAsync(cpath, cacheData).catch((err) => {
  122. winston.error('Unable to write to cache! Performance may be affected.');
  123. return true;
  124. });
  125. } else {
  126. return true;
  127. }
  128. }).return(pageData);
  129. });
  130. } else {
  131. return false;
  132. }
  133. }).catch((err) => {
  134. return Promise.reject(new Error('Entry ' + entryPath + ' does not exist!'));
  135. });
  136. },
  137. /**
  138. * Parse raw url path and make it safe
  139. *
  140. * @param {String} urlPath The url path
  141. * @return {String} Safe entry path
  142. */
  143. parsePath(urlPath) {
  144. let wlist = new RegExp('[^a-z0-9/\-]','g');
  145. urlPath = _.toLower(urlPath).replace(wlist, '');
  146. if(urlPath === '/') {
  147. urlPath = 'home';
  148. }
  149. let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p); });
  150. return _.join(urlParts, '/');
  151. },
  152. /**
  153. * Gets the parent information.
  154. *
  155. * @param {String} entryPath The entry path
  156. * @return {Promise<Object|False>} The parent information.
  157. */
  158. getParentInfo(entryPath) {
  159. let self = this;
  160. if(_.includes(entryPath, '/')) {
  161. let parentParts = _.initial(_.split(entryPath, '/'));
  162. let parentPath = _.join(parentParts,'/');
  163. let parentFile = _.last(parentParts);
  164. let fpath = self.getFullPath(parentPath);
  165. return fs.statAsync(fpath).then((st) => {
  166. if(st.isFile()) {
  167. return fs.readFileAsync(fpath, 'utf8').then((contents) => {
  168. let pageMeta = mark.parseMeta(contents);
  169. return {
  170. path: parentPath,
  171. title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
  172. subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
  173. };
  174. });
  175. } else {
  176. return Promise.reject(new Error('Parent entry is not a valid file.'));
  177. }
  178. });
  179. } else {
  180. return Promise.reject(new Error('Parent entry is root.'));
  181. }
  182. },
  183. /**
  184. * Gets the full original path of a document.
  185. *
  186. * @param {String} entryPath The entry path
  187. * @return {String} The full path.
  188. */
  189. getFullPath(entryPath) {
  190. return path.join(this._repoPath, entryPath + '.md');
  191. },
  192. /**
  193. * Gets the full cache path of a document.
  194. *
  195. * @param {String} entryPath The entry path
  196. * @return {String} The full cache path.
  197. */
  198. getCachePath(entryPath) {
  199. return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.bson');
  200. },
  201. /**
  202. * Gets the entry path from full path.
  203. *
  204. * @param {String} fullPath The full path
  205. * @return {String} The entry path
  206. */
  207. getEntryPathFromFullPath(fullPath) {
  208. let absRepoPath = path.resolve(ROOTPATH, this._repoPath);
  209. return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'),'/').value();
  210. },
  211. /**
  212. * Update an existing document
  213. *
  214. * @param {String} entryPath The entry path
  215. * @param {String} contents The markdown-formatted contents
  216. * @return {Promise<Boolean>} True on success, false on failure
  217. */
  218. update(entryPath, contents) {
  219. let self = this;
  220. let fpath = self.getFullPath(entryPath);
  221. return fs.statAsync(fpath).then((st) => {
  222. if(st.isFile()) {
  223. return self.makePersistent(entryPath, contents).then(() => {
  224. return self.updateCache(entryPath);
  225. });
  226. } else {
  227. return Promise.reject(new Error('Entry does not exist!'));
  228. }
  229. }).catch((err) => {
  230. winston.error(err);
  231. return Promise.reject(new Error('Failed to save document.'));
  232. });
  233. },
  234. /**
  235. * Update local cache and search index
  236. *
  237. * @param {String} entryPath The entry path
  238. * @return {Promise} Promise of the operation
  239. */
  240. updateCache(entryPath) {
  241. let self = this;
  242. return self.fetchOriginal(entryPath, {
  243. parseMarkdown: true,
  244. parseMeta: true,
  245. parseTree: true,
  246. includeMarkdown: true,
  247. includeParentInfo: true,
  248. cache: true
  249. }).then((pageData) => {
  250. return {
  251. entryPath,
  252. meta: pageData.meta,
  253. parent: pageData.parent || {},
  254. text: mark.removeMarkdown(pageData.markdown)
  255. };
  256. }).then((content) => {
  257. ws.emit('searchAdd', {
  258. auth: WSInternalKey,
  259. content
  260. });
  261. return true;
  262. });
  263. },
  264. /**
  265. * Create a new document
  266. *
  267. * @param {String} entryPath The entry path
  268. * @param {String} contents The markdown-formatted contents
  269. * @return {Promise<Boolean>} True on success, false on failure
  270. */
  271. create(entryPath, contents) {
  272. let self = this;
  273. return self.exists(entryPath).then((docExists) => {
  274. if(!docExists) {
  275. return self.makePersistent(entryPath, contents).then(() => {
  276. return self.updateCache(entryPath);
  277. });
  278. } else {
  279. return Promise.reject(new Error('Entry already exists!'));
  280. }
  281. }).catch((err) => {
  282. winston.error(err);
  283. return Promise.reject(new Error('Something went wrong.'));
  284. });
  285. },
  286. /**
  287. * Makes a document persistent to disk and git repository
  288. *
  289. * @param {String} entryPath The entry path
  290. * @param {String} contents The markdown-formatted contents
  291. * @return {Promise<Boolean>} True on success, false on failure
  292. */
  293. makePersistent(entryPath, contents) {
  294. let self = this;
  295. let fpath = self.getFullPath(entryPath);
  296. return fs.outputFileAsync(fpath, contents).then(() => {
  297. return git.commitDocument(entryPath);
  298. });
  299. },
  300. /**
  301. * Generate a starter page content based on the entry path
  302. *
  303. * @param {String} entryPath The entry path
  304. * @return {Promise<String>} Starter content
  305. */
  306. getStarter(entryPath) {
  307. let self = this;
  308. let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')));
  309. return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => {
  310. return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle);
  311. });
  312. }
  313. };