entries.js 9.0 KB

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