const Promise = require('bluebird'); const fs = require('fs-extra'); const crypto = require('crypto'); const urlService = require('../url'); const debug = require('@tryghost/debug')('services:route-settings'); const errors = require('@tryghost/errors'); const tpl = require('@tryghost/tpl'); const config = require('../../../shared/config'); const bridge = require('../../../bridge'); const SettingsLoader = require('./settings-loader'); const parseYaml = require('./yaml-parser'); const SettingsPathManager = require('@tryghost/settings-path-manager'); const settingsPathManager = new SettingsPathManager({ type: 'routes', paths: [config.getContentPath('settings')] }); const settingsLoader = new SettingsLoader({ parseYaml, storageFolderPath: config.getContentPath('settings') }); const messages = { loadError: 'Could not load {filename} file.' }; /** * The `routes.yaml` file offers a way to configure your Ghost blog. It's currently a setting feature * we have added. That's why the `routes.yaml` file is treated as a "setting" right now. * If we want to add single permissions for this file (e.g. upload/download routes.yaml), we can add later. * * How does it work? * * - we first reset all url generators (each url generator belongs to one express router) * - we don't destroy the resources, we only release them (this avoids reloading all resources from the db again) * - then we reload the whole site app, which will reset all routers and re-create the url generators */ class RouteSettings { constructor() { /** * md5 hashes of default routes settings * @private */ this.defaultRoutesSettingHash = '3d180d52c663d173a6be791ef411ed01'; /** * @private */ this.filename = 'routes'; /** * @private */ this.ext = 'yaml'; } /** * @private * @param {String} settingsPath * @param {String} backupPath */ async createBackupFile(settingsPath, backupPath) { return await fs.copy(settingsPath, backupPath); } /** * @private * @param {String} settingsPath * @param {String} backupPath */ async restoreBackupFile(settingsPath, backupPath) { return await fs.copy(backupPath, settingsPath); } /** * @private * @param {String} filePath * @param {String} settingsPath */ async saveFile(filePath, settingsPath) { return await fs.copy(filePath, settingsPath); } /** * @private * @param {String} settingsFilePath */ async readFile(settingsFilePath) { return fs.readFile(settingsFilePath, 'utf-8') .catch((err) => { if (err.code === 'ENOENT') { return Promise.resolve([]); } if (errors.utils.isIgnitionError(err)) { throw err; } throw new errors.NotFoundError({ err: err }); }); } async setFromFilePath(filePath) { const settingsPath = settingsPathManager.getDefaultFilePath(); const backupPath = settingsPathManager.getBackupFilePath(); await this.createBackupFile(settingsPath, backupPath); await this.saveFile(filePath, settingsPath); urlService.resetGenerators({releaseResourcesOnly: true}); const bringBackValidRoutes = async () => { urlService.resetGenerators({releaseResourcesOnly: true}); await this.restoreBackupFile(settingsPath, backupPath); return bridge.reloadFrontend(); }; try { bridge.reloadFrontend(); } catch (err) { return bringBackValidRoutes() .finally(() => { throw err; }); } // @TODO: how can we get rid of this from here? let tries = 0; function isBlogRunning() { debug('waiting for blog running'); return Promise.delay(1000) .then(() => { debug('waited for blog running'); if (!urlService.hasFinished()) { if (tries > 5) { throw new errors.InternalServerError({ message: tpl(messages.loadError, {filename: `${this.filename}.${this.ext}`}) }); } tries = tries + 1; return isBlogRunning(); } }); } return isBlogRunning() .catch((err) => { return bringBackValidRoutes() .finally(() => { throw err; }); }); } async get() { const settingsFilePath = settingsPathManager.getDefaultFilePath(); return this.readFile(settingsFilePath); } calculateHash(data) { return crypto.createHash('md5') .update(data, 'binary') .digest('hex'); } getDefaultHash() { return this.defaultRoutesSettingHash; } async getCurrentHash() { const data = await settingsLoader.loadSettings(); return this.calculateHash(JSON.stringify(data)); } } module.exports = RouteSettings;