mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
✨ Supported dynamic image resizing for LocalFileStorage(#10184)
refs #10181 * Added initial handleImageSizes middleware * Implemented saveRaw method on local file storage * Wired up handleImageSizes middleware * Implemented delete for LocalFileStorage * Removed delete method from theme Storage class * Deleted sizes directory when theme is activated * Ensured that smaller images are not enlarged * Renamed sizes -> size * Exited middleware as early as possible * Called getStorage as late as possible * Updated image sizes middleware to handle dimension paths * Revert "Deleted sizes directory when theme is activated" This reverts commit 9204dfcc73a6a79d597dbf23651817bcbfc59991. * Revert "Removed delete method from theme Storage class" This reverts commit b45fdb405a05faeaf4bd87e977c4ac64ff96b057. * Revert "Implemented delete for LocalFileStorage" This reverts commit a587cd6bae45b68a293b2d5cfd9b7705a29e7bfa. * Fixed typo Co-Authored-By: allouis <fabien@allou.is> * Redirected to original image if no image_sizes config * Refactored redirection because rule of three * Updated comments * Added rubbish tests * Added @TODO comment for handleImageSizes tests * Added safeResizeImage method to image manipulator * Used image manipulator lib in image_size middleware
This commit is contained in:
parent
2860ddeb3b
commit
7099dd45a5
6 changed files with 160 additions and 1 deletions
|
@ -19,6 +19,31 @@ class LocalFileStore extends StorageBase {
|
|||
this.storagePath = config.getContentPath('images');
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a buffer in the targetPath
|
||||
* - buffer is an instance of Buffer
|
||||
* - returns a Promise which returns the full URL to retrieve the data
|
||||
*/
|
||||
saveRaw(buffer, targetPath) {
|
||||
const storagePath = path.join(this.storagePath, targetPath);
|
||||
const targetDir = path.dirname(storagePath);
|
||||
|
||||
return fs.mkdirs(targetDir)
|
||||
.then(() => {
|
||||
return fs.writeFile(storagePath, buffer);
|
||||
})
|
||||
.then(() => {
|
||||
// For local file system storage can use relative path so add a slash
|
||||
const fullUrl = (
|
||||
urlService.utils.urlJoin('/', urlService.utils.getSubdir(),
|
||||
urlService.utils.STATIC_IMAGE_URL_PREFIX,
|
||||
targetPath)
|
||||
).replace(new RegExp(`\\${path.sep}`, 'g'), '/');
|
||||
|
||||
return fullUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the image to storage (the file system)
|
||||
* - image is the express image object
|
||||
|
|
|
@ -61,4 +61,27 @@ const process = (options = {}) => {
|
|||
});
|
||||
};
|
||||
|
||||
const resizeImage = (originalBuffer, {width, height} = {}) => {
|
||||
const sharp = require('sharp');
|
||||
return sharp(originalBuffer)
|
||||
.resize(width, height, {
|
||||
// CASE: dont make the image bigger than it was
|
||||
withoutEnlargement: true
|
||||
})
|
||||
// CASE: Automatically remove metadata and rotate based on the orientation.
|
||||
.rotate()
|
||||
.toBuffer()
|
||||
.then((resizedBuffer) => {
|
||||
return resizedBuffer.length < originalBuffer.length ? resizedBuffer : originalBuffer;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.process = process;
|
||||
module.exports.safeResizeImage = (buffer, options) => {
|
||||
try {
|
||||
require('sharp');
|
||||
return resizeImage(buffer, options);
|
||||
} catch (e) {
|
||||
return Promise.resolve(buffer);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
const path = require('path');
|
||||
const image = require('../../../../lib/image');
|
||||
const storage = require('../../../../adapters/storage');
|
||||
const activeTheme = require('../../../../services/themes/active');
|
||||
|
||||
const SIZE_PATH_REGEX = /^\/size\/([^/]+)\//;
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
if (!SIZE_PATH_REGEX.test(req.url)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const [sizeImageDir, requestedDimension] = req.url.match(SIZE_PATH_REGEX);
|
||||
const redirectToOriginal = () => {
|
||||
const url = req.originalUrl.replace(`/size/${requestedDimension}`, '');
|
||||
return res.redirect(url);
|
||||
};
|
||||
|
||||
const imageSizes = activeTheme.get().config('image_sizes');
|
||||
// CASE: no image_sizes config
|
||||
if (!imageSizes) {
|
||||
return redirectToOriginal();
|
||||
}
|
||||
|
||||
const imageDimensions = Object.keys(imageSizes).reduce((dimensions, size) => {
|
||||
const {width, height} = imageSizes[size];
|
||||
const dimension = (width ? 'w' + width : '') + (height ? 'h' + height : '');
|
||||
return Object.assign({
|
||||
[dimension]: imageSizes[size]
|
||||
}, dimensions);
|
||||
}, {});
|
||||
|
||||
const imageDimensionConfig = imageDimensions[requestedDimension];
|
||||
// CASE: unknown dimension
|
||||
if (!imageDimensionConfig || (!imageDimensionConfig.width && !imageDimensionConfig.height)) {
|
||||
return redirectToOriginal();
|
||||
}
|
||||
|
||||
const storageInstance = storage.getStorage();
|
||||
// CASE: unsupported storage adapter but theme is using custom image_sizes
|
||||
if (typeof storageInstance.saveRaw !== 'function') {
|
||||
return redirectToOriginal();
|
||||
}
|
||||
|
||||
storageInstance.exists(req.url).then((exists) => {
|
||||
if (exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalImagePath = path.relative(sizeImageDir, req.url);
|
||||
|
||||
return storageInstance.read({path: originalImagePath})
|
||||
.then((originalImageBuffer) => {
|
||||
return image.manipulator.safeResizeImage(originalImageBuffer, imageDimensionConfig);
|
||||
})
|
||||
.then((resizedImageBuffer) => {
|
||||
return storageInstance.saveRaw(resizedImageBuffer, req.url);
|
||||
});
|
||||
}).then(() => {
|
||||
next();
|
||||
}).catch(next);
|
||||
};
|
|
@ -1,5 +1,8 @@
|
|||
module.exports = {
|
||||
get normalize() {
|
||||
return require('./normalize');
|
||||
},
|
||||
get handleImageSizes() {
|
||||
return require('./handle-image-sizes');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -13,6 +13,8 @@ const themeMiddleware = require('../../services/themes').middleware;
|
|||
const siteRoutes = require('./routes');
|
||||
const shared = require('../shared');
|
||||
|
||||
const STATIC_IMAGE_URL_PREFIX = `/${urlService.utils.STATIC_IMAGE_URL_PREFIX}`;
|
||||
|
||||
let router;
|
||||
|
||||
function SiteRouter(req, res, next) {
|
||||
|
@ -58,7 +60,7 @@ module.exports = function setupSiteApp(options = {}) {
|
|||
siteApp.use(shared.middlewares.servePublicFile('public/404-ghost.png', 'png', constants.ONE_HOUR_S));
|
||||
|
||||
// Serve blog images using the storage adapter
|
||||
siteApp.use('/' + urlService.utils.STATIC_IMAGE_URL_PREFIX, storage.getStorage().serve());
|
||||
siteApp.use(STATIC_IMAGE_URL_PREFIX, shared.middlewares.image.handleImageSizes, storage.getStorage().serve());
|
||||
|
||||
// @TODO find this a better home
|
||||
// We do this here, at the top level, because helpers require so much stuff.
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
const should = require('should');
|
||||
const handleImageSizes = require('../../../../../server/web/shared/middlewares/image/handle-image-sizes.js');
|
||||
|
||||
// @TODO make these tests lovely and non specific to implementation
|
||||
describe('handleImageSizes middleware', function () {
|
||||
it('calls next immediately if the url does not match /size/something/', function (done) {
|
||||
const fakeReq = {
|
||||
url: '/size/something'
|
||||
};
|
||||
// CASE: second thing middleware does is try to match to a regex
|
||||
fakeReq.url.match = function () {
|
||||
throw new Error('Should have exited immediately');
|
||||
};
|
||||
handleImageSizes(fakeReq, {}, function next() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls next immediately if the url does not match /size/something/', function (done) {
|
||||
const fakeReq = {
|
||||
url: '/url/whatever/'
|
||||
};
|
||||
// CASE: second thing middleware does is try to match to a regex
|
||||
fakeReq.url.match = function () {
|
||||
throw new Error('Should have exited immediately');
|
||||
};
|
||||
handleImageSizes(fakeReq, {}, function next() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls next immediately if the url does not match /size/something/', function (done) {
|
||||
const fakeReq = {
|
||||
url: '/size//'
|
||||
};
|
||||
// CASE: second thing middleware does is try to match to a regex
|
||||
fakeReq.url.match = function () {
|
||||
throw new Error('Should have exited immediately');
|
||||
};
|
||||
handleImageSizes(fakeReq, {}, function next() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue