| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 | 'use strict'const Promise = require('bluebird')const path = require('path')const fs = Promise.promisifyAll(require('fs-extra'))const _ = require('lodash')const crypto = require('crypto')/** * Entries Model */module.exports = {  _repoPath: 'repo',  _cachePath: 'data/cache',  /**   * Initialize Entries model   *   * @return     {Object}  Entries model instance   */  init () {    let self = this    self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)    self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache')    return self  },  /**   * Check if a document already exists   *   * @param      {String}  entryPath  The entry path   * @return     {Promise<Boolean>}  True if exists, false otherwise   */  exists (entryPath) {    let self = this    return self.fetchOriginal(entryPath, {      parseMarkdown: false,      parseMeta: false,      parseTree: false,      includeMarkdown: false,      includeParentInfo: false,      cache: false    }).then(() => {      return true    }).catch((err) => { // eslint-disable-line handle-callback-err      return false    })  },  /**   * Fetch a document from cache, otherwise the original   *   * @param      {String}           entryPath  The entry path   * @return     {Promise<Object>}  Page Data   */  fetch (entryPath) {    let self = this    let cpath = self.getCachePath(entryPath)    return fs.statAsync(cpath).then((st) => {      return st.isFile()    }).catch((err) => { // eslint-disable-line handle-callback-err      return false    }).then((isCache) => {      if (isCache) {        // Load from cache        return fs.readFileAsync(cpath).then((contents) => {          return JSON.parse(contents)        }).catch((err) => { // eslint-disable-line handle-callback-err          winston.error('Corrupted cache file. Deleting it...')          fs.unlinkSync(cpath)          return false        })      } else {        // Load original        return self.fetchOriginal(entryPath)      }    })  },  /**   * Fetches the original document entry   *   * @param      {String}           entryPath  The entry path   * @param      {Object}           options    The options   * @return     {Promise<Object>}  Page data   */  fetchOriginal (entryPath, options) {    let self = this    let fpath = self.getFullPath(entryPath)    let cpath = self.getCachePath(entryPath)    options = _.defaults(options, {      parseMarkdown: true,      parseMeta: true,      parseTree: true,      includeMarkdown: false,      includeParentInfo: true,      cache: true    })    return fs.statAsync(fpath).then((st) => {      if (st.isFile()) {        return fs.readFileAsync(fpath, 'utf8').then((contents) => {          // Parse contents          let pageData = {            markdown: (options.includeMarkdown) ? contents : '',            html: (options.parseMarkdown) ? mark.parseContent(contents) : '',            meta: (options.parseMeta) ? mark.parseMeta(contents) : {},            tree: (options.parseTree) ? mark.parseTree(contents) : []          }          if (!pageData.meta.title) {            pageData.meta.title = _.startCase(entryPath)          }          pageData.meta.path = entryPath          // Get parent          let parentPromise = (options.includeParentInfo) ? self.getParentInfo(entryPath).then((parentData) => {            return (pageData.parent = parentData)          }).catch((err) => { // eslint-disable-line handle-callback-err            return (pageData.parent = false)          }) : Promise.resolve(true)          return parentPromise.then(() => {            // Cache to disk            if (options.cache) {              let cacheData = JSON.stringify(_.pick(pageData, ['html', 'meta', 'tree', 'parent']), false, false, false)              return fs.writeFileAsync(cpath, cacheData).catch((err) => {                winston.error('Unable to write to cache! Performance may be affected.')                winston.error(err)                return true              })            } else {              return true            }          }).return(pageData)        })      } else {        return false      }    }).catch((err) => { // eslint-disable-line handle-callback-err      throw new Promise.OperationalError('Entry ' + entryPath + ' does not exist!')    })  },  /**   * Parse raw url path and make it safe   *   * @param      {String}  urlPath  The url path   * @return     {String}  Safe entry path   */  parsePath (urlPath) {    let wlist = new RegExp('[^a-z0-9/-]', 'g')    urlPath = _.toLower(urlPath).replace(wlist, '')    if (urlPath === '/') {      urlPath = 'home'    }    let urlParts = _.filter(_.split(urlPath, '/'), (p) => { return !_.isEmpty(p) })    return _.join(urlParts, '/')  },  /**   * Gets the parent information.   *   * @param      {String}                 entryPath  The entry path   * @return     {Promise<Object|False>}  The parent information.   */  getParentInfo (entryPath) {    let self = this    if (_.includes(entryPath, '/')) {      let parentParts = _.initial(_.split(entryPath, '/'))      let parentPath = _.join(parentParts, '/')      let parentFile = _.last(parentParts)      let fpath = self.getFullPath(parentPath)      return fs.statAsync(fpath).then((st) => {        if (st.isFile()) {          return fs.readFileAsync(fpath, 'utf8').then((contents) => {            let pageMeta = mark.parseMeta(contents)            return {              path: parentPath,              title: (pageMeta.title) ? pageMeta.title : _.startCase(parentFile),              subtitle: (pageMeta.subtitle) ? pageMeta.subtitle : false            }          })        } else {          return Promise.reject(new Error('Parent entry is not a valid file.'))        }      })    } else {      return Promise.reject(new Error('Parent entry is root.'))    }  },  /**   * Gets the full original path of a document.   *   * @param      {String}  entryPath  The entry path   * @return     {String}  The full path.   */  getFullPath (entryPath) {    return path.join(this._repoPath, entryPath + '.md')  },  /**   * Gets the full cache path of a document.   *   * @param      {String}    entryPath  The entry path   * @return     {String}  The full cache path.   */  getCachePath (entryPath) {    return path.join(this._cachePath, crypto.createHash('md5').update(entryPath).digest('hex') + '.json')  },  /**   * Gets the entry path from full path.   *   * @param      {String}  fullPath  The full path   * @return     {String}  The entry path   */  getEntryPathFromFullPath (fullPath) {    let absRepoPath = path.resolve(ROOTPATH, this._repoPath)    return _.chain(fullPath).replace(absRepoPath, '').replace('.md', '').replace(new RegExp('\\\\', 'g'), '/').value()  },  /**   * Update an existing document   *   * @param      {String}            entryPath  The entry path   * @param      {String}            contents   The markdown-formatted contents   * @return     {Promise<Boolean>}  True on success, false on failure   */  update (entryPath, contents) {    let self = this    let fpath = self.getFullPath(entryPath)    return fs.statAsync(fpath).then((st) => {      if (st.isFile()) {        return self.makePersistent(entryPath, contents).then(() => {          return self.updateCache(entryPath).then(entry => {            return search.add(entry)          })        })      } else {        return Promise.reject(new Error('Entry does not exist!'))      }    }).catch((err) => {      winston.error(err)      return Promise.reject(new Error('Failed to save document.'))    })  },  /**   * Update local cache and search index   *   * @param      {String}   entryPath  The entry path   * @return     {Promise}  Promise of the operation   */  updateCache (entryPath) {    let self = this    return self.fetchOriginal(entryPath, {      parseMarkdown: true,      parseMeta: true,      parseTree: true,      includeMarkdown: true,      includeParentInfo: true,      cache: true    }).catch(err => {      winston.error(err)      return err    }).then((pageData) => {      return {        entryPath,        meta: pageData.meta,        parent: pageData.parent || {},        text: mark.removeMarkdown(pageData.markdown)      }    }).catch(err => {      winston.error(err)      return err    }).then((content) => {      return db.Entry.findOneAndUpdate({        _id: content.entryPath      }, {        _id: content.entryPath,        title: content.meta.title || content.entryPath,        subtitle: content.meta.subtitle || '',        parent: content.parent.title || '',        parentPath: content.parent.path || ''      }, {        new: true,        upsert: true      })    }).catch(err => {      winston.error(err)      return err    })  },  /**   * Create a new document   *   * @param      {String}            entryPath  The entry path   * @param      {String}            contents   The markdown-formatted contents   * @return     {Promise<Boolean>}  True on success, false on failure   */  create (entryPath, contents) {    let self = this    return self.exists(entryPath).then((docExists) => {      if (!docExists) {        return self.makePersistent(entryPath, contents).then(() => {          return self.updateCache(entryPath).then(entry => {            return search.add(entry)          })        })      } else {        return Promise.reject(new Error('Entry already exists!'))      }    }).catch((err) => {      winston.error(err)      return Promise.reject(new Error('Something went wrong.'))    })  },  /**   * Makes a document persistent to disk and git repository   *   * @param      {String}            entryPath  The entry path   * @param      {String}            contents   The markdown-formatted contents   * @return     {Promise<Boolean>}  True on success, false on failure   */  makePersistent (entryPath, contents) {    let self = this    let fpath = self.getFullPath(entryPath)    return fs.outputFileAsync(fpath, contents).then(() => {      return git.commitDocument(entryPath)    })  },  /**   * Move a document   *   * @param      {String}   entryPath     The current entry path   * @param      {String}   newEntryPath  The new entry path   * @return     {Promise}  Promise of the operation   */  move (entryPath, newEntryPath) {    let self = this    if (_.isEmpty(entryPath) || entryPath === 'home') {      return Promise.reject(new Error('Invalid path!'))    }    return git.moveDocument(entryPath, newEntryPath).then(() => {      return git.commitDocument(newEntryPath).then(() => {        // Delete old cache version        let oldEntryCachePath = self.getCachePath(entryPath)        fs.unlinkAsync(oldEntryCachePath).catch((err) => { return true }) // eslint-disable-line handle-callback-err        // Delete old index entry        search.delete(entryPath)        // Create cache for new entry        return self.updateCache(newEntryPath).then(entry => {          return search.add(entry)        })      })    })  },  /**   * Generate a starter page content based on the entry path   *   * @param      {String}           entryPath  The entry path   * @return     {Promise<String>}  Starter content   */  getStarter (entryPath) {    let formattedTitle = _.startCase(_.last(_.split(entryPath, '/')))    return fs.readFileAsync(path.join(ROOTPATH, 'client/content/create.md'), 'utf8').then((contents) => {      return _.replace(contents, new RegExp('{TITLE}', 'g'), formattedTitle)    })  },  /**   * Get all entries from base path   *   * @param {String} basePath Path to list from   * @return {Promise<Array>} List of entries   */  getFromTree (basePath) {    return Promise.resolve([])  }}
 |