mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-04 12:44:57 +03:00
d541a14826
- Currently theme uploads delete the existing theme before copying the new files into place - If something goes wrong with the delete action, you will end up in a bad state - Some or all of the files may be deleted, but now Ghost won't try to put the new theme in place, instead returning an error - This leaves you with an invalid active theme and a broken site - Unlike delete, move is a one-hit operation that succeeds or fails, there moving a theme is safer than deleting - This updated code moves the old theme to a folder with the name [theme-name]-[uuid] before copying the new theme into place - Even if this fails, the files should not be gone - There's a cleanup operation to remove the theme backup at the end, but we don't care too much if this fails
134 lines
4.4 KiB
JavaScript
134 lines
4.4 KiB
JavaScript
const fs = require('fs-extra');
|
|
|
|
const activate = require('./activate');
|
|
const validate = require('./validate');
|
|
const list = require('./list');
|
|
const ThemeStorage = require('./ThemeStorage');
|
|
const themeLoader = require('./loader');
|
|
const toJSON = require('./to-json');
|
|
|
|
const settingsCache = require('../../../server/services/settings/cache');
|
|
const {i18n} = require('../../../server/lib/common');
|
|
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;
|
|
|
|
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) {
|
|
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);
|
|
activate(loadedTheme, checkedTheme);
|
|
}
|
|
|
|
// @TODO: unify the name across gscan and Ghost!
|
|
return {
|
|
themeOverridden: overrideTheme,
|
|
theme: toJSON(shortName, checkedTheme)
|
|
};
|
|
})
|
|
.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);
|
|
});
|
|
}
|
|
};
|