Ghost/core/server/services/route-settings/route-settings.js
Naz cbec6aa49e Refactored route settings module into class
refs https://linear.app/tryghost/issue/CORE-35/refactor-route-and-redirect-settings

- It's a step to making the module follow class+DI pattern before fully extracting it into an external libarary
- Reminder, doing in Ghost repo instead of substituting big chunks all at once to have clear history of how the service evolved prior to the extraction into external lib!
2021-11-24 05:32:20 +13:00

186 lines
5.3 KiB
JavaScript

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;