Ghost/core/server/services/themes/storage.js
Hannah Wolfe 829e8ed010 Expanded requires of lib/common i18n and events
- Having these as destructured from the same package is hindering refactoring now
- Events should really only ever be used server-side
- i18n should be a shared module for now so it can be used everywhere until we figure out something better
- Having them seperate also allows us to lint them properly
2021-05-03 17:14:52 +01:00

150 lines
5.0 KiB
JavaScript

const fs = require('fs-extra');
const bridge = require('../../../bridge');
const validate = require('./validate');
const list = require('./list');
const ThemeStorage = require('./ThemeStorage');
const themeLoader = require('./loader');
const toJSON = require('./to-json');
const settingsCache = require('../settings/cache');
const i18n = require('../../../server/lib/common/i18n');
const logging = require('../../../shared/logging');
const errors = require('@tryghost/errors');
const debug = require('ghost-ignition').debug('api:themes');
const ObjectID = require('bson-objectid');
let themeStorage;
const getStorage = () => {
themeStorage = themeStorage || new ThemeStorage();
return themeStorage;
};
module.exports = {
getZip: (themeName) => {
const theme = list.get(themeName);
if (!theme) {
return Promise.reject(new errors.BadRequestError({
message: i18n.t('errors.api.themes.invalidThemeName')
}));
}
return getStorage().serve({
name: themeName
});
},
setFromZip: (zip) => {
const shortName = getStorage().getSanitizedFileName(zip.name.split('.zip')[0]);
const backupName = `${shortName}_${ObjectID()}`;
// check if zip name is casper.zip
if (zip.name === 'casper.zip') {
throw new errors.ValidationError({
message: i18n.t('errors.api.themes.overrideCasper')
});
}
let checkedTheme;
let renamedExisting = false;
return validate.checkSafe(zip, true)
.then((_checkedTheme) => {
checkedTheme = _checkedTheme;
return getStorage().exists(shortName);
})
.then((themeExists) => {
// CASE: move the existing theme to a backup folder
if (themeExists) {
renamedExisting = true;
return getStorage().rename(shortName, backupName);
}
})
.then(() => {
// CASE: store extracted theme
return getStorage().save({
name: shortName,
path: checkedTheme.path
});
})
.then(() => {
// CASE: loads the theme from the fs & sets the theme on the themeList
return themeLoader.loadOneTheme(shortName);
})
.then((loadedTheme) => {
const overrideTheme = (shortName === settingsCache.get('active_theme'));
// CASE: if this is the active theme, we are overriding
if (overrideTheme) {
debug('Activating theme (method C, on API "override")', shortName);
bridge.activateTheme(loadedTheme, checkedTheme);
}
// @TODO: unify the name across gscan and Ghost!
return {
themeOverridden: overrideTheme,
theme: toJSON(shortName, checkedTheme)
};
})
.catch((error) => {
// restore backup if we renamed an existing theme but saving failed
if (renamedExisting) {
return getStorage().exists(shortName).then((themeExists) => {
if (!themeExists) {
return getStorage().rename(backupName, shortName).then(() => {
throw error;
});
}
});
}
throw error;
})
.finally(() => {
// @TODO: we should probably do this as part of saving the theme
// CASE: remove extracted dir from gscan happens in background
if (checkedTheme) {
fs.remove(checkedTheme.path)
.catch((err) => {
logging.error(new errors.GhostError({err: err}));
});
}
// CASE: remove the backup we created earlier
getStorage()
.delete(backupName)
.catch((err) => {
logging.error(new errors.GhostError({err: err}));
});
});
},
destroy: function (themeName) {
if (themeName === 'casper') {
throw new errors.ValidationError({
message: i18n.t('errors.api.themes.destroyCasper')
});
}
if (themeName === settingsCache.get('active_theme')) {
throw new errors.ValidationError({
message: i18n.t('errors.api.themes.destroyActive')
});
}
const theme = list.get(themeName);
if (!theme) {
throw new errors.NotFoundError({
message: i18n.t('errors.api.themes.themeDoesNotExist')
});
}
return getStorage().delete(themeName)
.then(() => {
list.del(themeName);
});
}
};