0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Change theme uploads to move & delete at end

- 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
This commit is contained in:
Hannah Wolfe 2020-06-03 16:29:34 +01:00
parent a97996eec1
commit d541a14826
3 changed files with 45 additions and 18 deletions

View file

@ -57,8 +57,26 @@ class ThemeStorage extends LocalFileStorage {
};
}
/**
* Rename a file / folder
*
*
* @param String fileName
*/
rename(srcName, destName) {
let src = path.join(this.getTargetDir(), srcName);
let dest = path.join(this.getTargetDir(), destName);
return fs.move(src, dest);
}
/**
* Rename a file / folder
*
* @param String backupName
*/
delete(fileName) {
return fs.remove(path.join(this.storagePath, fileName));
return fs.remove(path.join(this.getTargetDir(), fileName));
}
}

View file

@ -12,6 +12,7 @@ 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;
@ -37,6 +38,7 @@ module.exports = {
},
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') {
@ -54,9 +56,9 @@ module.exports = {
return getStorage().exists(shortName);
})
.then((themeExists) => {
// CASE: delete existing theme
// CASE: move the existing theme to a backup folder
if (themeExists) {
return getStorage().delete(shortName);
return getStorage().rename(shortName, backupName);
}
})
.then(() => {
@ -86,14 +88,20 @@ module.exports = {
})
.finally(() => {
// @TODO: we should probably do this as part of saving the theme
// CASE: remove extracted dir from gscan
// happens in background
// 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) {

View file

@ -118,20 +118,11 @@ describe('Themes API', function () {
tmpFolderContents.splice(index, 1);
}
});
tmpFolderContents.should.be.an.Array().with.lengthOf(10);
tmpFolderContents.should.eql([
'broken-theme',
'casper',
'casper-1.4',
'casper.zip',
'invalid.zip',
'test-theme',
'test-theme-channels',
'valid',
'valid.zip',
'warnings.zip'
]);
// Note: at this point, the tmpFolder can legitimately still contain a valid_34324324 backup
// As it is deleted asynchronously
tmpFolderContents.should.containEql('valid');
tmpFolderContents.should.containEql('valid.zip');
// Check the Themes API returns the correct result
return ownerRequest
@ -157,6 +148,16 @@ describe('Themes API', function () {
should.exist(addedTheme);
localUtils.API.checkResponse(addedTheme, 'theme');
addedTheme.active.should.be.false();
// Note: at this point, the API should not return a valid_34324324 backup folder as a theme
_.map(jsonResponse.themes, 'name').should.eql([
'broken-theme',
'casper',
'casper-1.4',
'test-theme',
'test-theme-channels',
'valid'
]);
});
});