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.

390 lines
9.0 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(pageData, 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. * Fetches a text version of a Markdown-formatted document
  139. *
  140. * @param {String} entryPath The entry path
  141. * @return {String} Text-only version
  142. */
  143. fetchIndexableVersion(entryPath) {
  144. let self = this;
  145. return self.fetchOriginal(entryPath, {
  146. parseMarkdown: false,
  147. parseMeta: true,
  148. parseTree: false,
  149. includeMarkdown: true,
  150. includeParentInfo: true,
  151. cache: false
  152. }).then((pageData) => {
  153. return {
  154. entryPath,
  155. meta: pageData.meta,
  156. parent: pageData.parent || {},
  157. text: mark.removeMarkdown(pageData.markdown)
  158. };
  159. });
  160. },
  161. /**
  162. * Parse raw url path and make it safe
  163. *
  164. * @param {String} urlPath The url path
  165. * @return {String} Safe entry path
  166. */
  167. parsePath(urlPath) {
  168. let wlist = new RegExp('[^a-z0-9/\-]','g');
  169. urlPath = _.toLower(urlPath).replace(wlist, '');
  170. if(urlPath === '/') {
  171. urlPath = 'home';
  172. }
  173. let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p); });
  174. return _.join(urlParts, '/');
  175. },
  176. /**
  177. * Gets the parent information.
  178. *
  179. * @param {String} entryPath The entry path
  180. * @return {Promise<Object|False>} The parent information.
  181. */
  182. getParentInfo(entryPath) {
  183. let self = this;
  184. if(_.includes(entryPath, '/')) {
  185. let parentParts = _.initial(_.split(entryPath, '/'));
  186. let parentPath = _.join(parentParts,'/');
  187. let parentFile = _.last(parentParts);
  188. let fpath = self.getFullPath(parentPath);
  189. return fs.statAsync(fpath).then((st) => {
  190. if(st.isFile()) {
  191. return fs.readFileAsync(fpath, 'utf8').then((contents) => {
  192. let pageMeta = mark.parseMeta(contents);
  193. return {
  194. path: parentPath,
  195. title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),
  196. subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false
  197. };
  198. });
  199. } else {
  200. return Promise.reject(new Error('Parent entry is not a valid file.'));
  201. }
  202. });
  203. } else {
  204. return Promise.reject(new Error('Parent entry is root.'));
  205. }
  206. },
  207. /**
  208. * Gets the full original path of a document.
  209. *
  210. * @param {String} entryPath The entry path
  211. * @return {String} The full path.
  212. */
  213. getFullPath(entryPath) {
  214. return path.join(this._repoPath, entryPath + '.md');
  215. },
  216. /**
  217. * Gets the full cache path of a document.
  218. *
  219. * @param {String} entryPath The entry path
  220. * @return {String} The full cache path.
  221. */
  222. getCachePath(entryPath) {
  223. return path.join(this._cachePath, farmhash.fingerprint32(entryPath) + '.bson');
  224. },
  225. /**
  226. * Gets the entry path from full path.
  227. *
  228. * @param {String} fullPath The full path
  229. * @return {String} The entry path
  230. */
  231. getEntryPathFromFullPath(fullPath) {
  232. let absRepoPath = path.resolve(ROOTPATH, this._repoPath);
  233. return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'),'/').value();
  234. },
  235. /**
  236. * Update an existing document
  237. *
  238. * @param {String} entryPath The entry path
  239. * @param {String} contents The markdown-formatted contents
  240. * @return {Promise<Boolean>} True on success, false on failure
  241. */
  242. update(entryPath, contents) {
  243. let self = this;
  244. let fpath = self.getFullPath(entryPath);
  245. return fs.statAsync(fpath).then((st) => {
  246. if(st.isFile()) {
  247. return self.makePersistent(entryPath, contents).then(() => {
  248. return self.fetchOriginal(entryPath, {});
  249. });
  250. } else {
  251. return Promise.reject(new Error('Entry does not exist!'));
  252. }
  253. }).catch((err) => {
  254. return Promise.reject(new Error('Entry does not exist!'));
  255. });
  256. },
  257. /**
  258. * Create a new document
  259. *
  260. * @param {String} entryPath The entry path
  261. * @param {String} contents The markdown-formatted contents
  262. * @return {Promise<Boolean>} True on success, false on failure
  263. */
  264. create(entryPath, contents) {
  265. let self = this;
  266. return self.exists(entryPath).then((docExists) => {
  267. if(!docExists) {
  268. return self.makePersistent(entryPath, contents).then(() => {
  269. return self.fetchOriginal(entryPath, {});
  270. });
  271. } else {
  272. return Promise.reject(new Error('Entry already exists!'));
  273. }
  274. }).catch((err) => {
  275. winston.error(err);
  276. return Promise.reject(new Error('Something went wrong.'));
  277. });
  278. },
  279. /**
  280. * Makes a document persistent to disk and git repository
  281. *
  282. * @param {String} entryPath The entry path
  283. * @param {String} contents The markdown-formatted contents
  284. * @return {Promise<Boolean>} True on success, false on failure
  285. */
  286. makePersistent(entryPath, contents) {
  287. let self = this;
  288. let fpath = self.getFullPath(entryPath);
  289. return fs.outputFileAsync(fpath, contents).then(() => {
  290. return git.commitDocument(entryPath);
  291. });
  292. },
  293. /**
  294. * Generate a starter page content based on the entry path
  295. *
  296. * @param {String} entryPath The entry path
  297. * @return {Promise<String>} Starter content
  298. */
  299. getStarter(entryPath) {
  300. let self = this;
  301. let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')));
  302. return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => {
  303. return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle);
  304. });
  305. }
  306. };