mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-08 04:03:12 +03:00
a45a91c906
refs #8868 * 📐 Use request util in image-size - swapped the usage of `got` for requests with the request util * 💄 Use catch predicates - Uses catch predicates instead of conditionals in `getImageSizeFromUrl` - Return `NotFoundError` if applicable in `getImageSizeFromFilePath` as the caller function `cachedImageSizeFromUrl` is differentiating those between this error and others. * 🐛 Fixed ImageObject URL & simplify no protocol URL logic - Using `ImageObject` as a global var resulted in having the `url` property being the same for all requests coming in. - The logic that checked for an existing protocol (e. g. gravatar URLs) was overly complicated. Refactored it to be more simple. - Passing the correct value to `fetchDimensionsFromBuffer` as the population of `imageObject.url` happens there. These are used in our structured data and need to be full URLs (in case of locally stored files) or the original URL (in case of URLs missing the protocol) - Added two more debug logs in `getCachedImageSizeFromUrl` so it's logged when an image is added to the cache even tho it was returned as error. * 👀 Differentiate error codes between request and storage * 🔥 Remove not needed `Promise.resolve()` We're always resolving the result in `getCachedImageSizeFromUrl`, so there's no need to return the values with a `Promise.resolve()`. The caller fn uses waits for the Promises to be fulfilled. * ☂️ Wrap already rejected predicate errors in catch all * Use errorDetails instead of context * ☂️ Support /assets/ image paths - adds a guard that checks the image URL for `/assets/` in the beginning and passes a completed URL to the request util to try and fetch the image size - adds tests
236 lines
8.2 KiB
JavaScript
236 lines
8.2 KiB
JavaScript
var debug = require('ghost-ignition').debug('utils:image-size'),
|
|
sizeOf = require('image-size'),
|
|
url = require('url'),
|
|
Promise = require('bluebird'),
|
|
request = require('../utils/request'),
|
|
utils = require('../utils'),
|
|
errors = require('../errors'),
|
|
config = require('../config'),
|
|
storage = require('../adapters/storage'),
|
|
_ = require('lodash'),
|
|
storageUtils = require('../adapters/storage/utils'),
|
|
getImageSizeFromUrl,
|
|
getImageSizeFromFilePath;
|
|
|
|
/**
|
|
* @description compares the imagePath with a regex that reflects our local file storage
|
|
* @param {String} imagePath as URL or filepath
|
|
* @returns {Array} if match is true or null if not
|
|
*/
|
|
function isLocalImage(imagePath) {
|
|
imagePath = utils.url.urlFor('image', {image: imagePath}, true);
|
|
|
|
if (imagePath) {
|
|
return imagePath.match(new RegExp('^' + utils.url.urlJoin(utils.url.urlFor('home', true), utils.url.getSubdir(), '/', utils.url.STATIC_IMAGE_URL_PREFIX)));
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description processes the Buffer result of an image file
|
|
* @param {Object} options
|
|
* @returns {Object} dimensions
|
|
*/
|
|
function fetchDimensionsFromBuffer(options) {
|
|
var buffer = options.buffer,
|
|
imagePath = options.imagePath,
|
|
imageObject = {},
|
|
dimensions;
|
|
|
|
imageObject.url = imagePath;
|
|
|
|
try {
|
|
// Using the Buffer rather than an URL requires to use sizeOf synchronously.
|
|
// See https://github.com/image-size/image-size#asynchronous
|
|
dimensions = sizeOf(buffer);
|
|
|
|
// CASE: `.ico` files might have multiple images and therefore multiple sizes.
|
|
// We return the largest size found (image-size default is the first size found)
|
|
if (dimensions.images) {
|
|
dimensions.width = _.maxBy(dimensions.images, function (w) {return w.width;}).width;
|
|
dimensions.height = _.maxBy(dimensions.images, function (h) {return h.height;}).height;
|
|
}
|
|
|
|
imageObject.width = dimensions.width;
|
|
imageObject.height = dimensions.height;
|
|
|
|
return Promise.resolve(imageObject);
|
|
} catch (err) {
|
|
return Promise.reject(new errors.InternalServerError({
|
|
code: 'IMAGE_SIZE',
|
|
err: err,
|
|
context: imagePath
|
|
}));
|
|
}
|
|
}
|
|
|
|
// Supported formats of https://github.com/image-size/image-size:
|
|
// BMP, GIF, JPEG, PNG, PSD, TIFF, WebP, SVG, ICO
|
|
// ***
|
|
// Takes the url of the image and an optional timeout
|
|
// getImageSizeFromUrl returns an Object like this
|
|
// {
|
|
// height: 50,
|
|
// url: 'http://myblog.com/images/cat.jpg',
|
|
// width: 50
|
|
// };
|
|
// if the dimensions can be fetched, and rejects with error, if not.
|
|
// ***
|
|
// In case we get a locally stored image, which is checked withing the `isLocalImage`
|
|
// function we switch to read the image from the local file storage with `getImageSizeFromFilePath`.
|
|
// In case the image is not stored locally and is missing the protocol (like //www.gravatar.com/andsoon),
|
|
// we add the protocol and use urlFor() to get the absolute URL.
|
|
// If the request fails or image-size is not able to read the file, we reject with error.
|
|
|
|
/**
|
|
* @description read image dimensions from URL
|
|
* @param {String} imagePath as URL
|
|
* @returns {Promise<Object>} imageObject or error
|
|
*/
|
|
getImageSizeFromUrl = function getImageSizeFromUrl(imagePath) {
|
|
var requestOptions,
|
|
parsedUrl,
|
|
timeout = config.get('times:getImageSizeTimeoutInMS') || 10000;
|
|
|
|
if (isLocalImage(imagePath)) {
|
|
// don't make a request for a locally stored image
|
|
return getImageSizeFromFilePath(imagePath);
|
|
}
|
|
|
|
// CASE: pre 1.0 users were able to use an asset path for their blog logo
|
|
if (imagePath.match(/^\/assets/)) {
|
|
imagePath = utils.url.urlJoin(utils.url.urlFor('home', true), utils.url.getSubdir(), '/', imagePath);
|
|
}
|
|
|
|
parsedUrl = url.parse(imagePath);
|
|
|
|
// check if we got an url without any protocol
|
|
if (!parsedUrl.protocol) {
|
|
// CASE: our gravatar URLs start with '//' and we need to add 'http:'
|
|
// to make the request work
|
|
imagePath = 'http:' + imagePath;
|
|
}
|
|
|
|
debug('requested imagePath:', imagePath);
|
|
requestOptions = {
|
|
headers: {
|
|
'User-Agent': 'Mozilla/5.0'
|
|
},
|
|
timeout: timeout,
|
|
encoding: null
|
|
};
|
|
|
|
return request(
|
|
imagePath,
|
|
requestOptions
|
|
).then(function (response) {
|
|
debug('Image fetched (URL):', imagePath);
|
|
|
|
return fetchDimensionsFromBuffer({
|
|
buffer: response.body,
|
|
// we need to return the URL that's accessible for network requests as this imagePath
|
|
// value will be used as the URL for structured data
|
|
imagePath: parsedUrl.href
|
|
});
|
|
}).catch({code: 'URL_MISSING_INVALID'}, function (err) {
|
|
return Promise.reject(new errors.InternalServerError({
|
|
message: err.message,
|
|
code: 'IMAGE_SIZE_URL',
|
|
statusCode: err.statusCode,
|
|
context: err.url || imagePath
|
|
}));
|
|
}).catch({code: 'ETIMEDOUT'}, {statusCode: 408}, function (err) {
|
|
return Promise.reject(new errors.InternalServerError({
|
|
message: 'Request timed out.',
|
|
code: 'IMAGE_SIZE_URL',
|
|
statusCode: err.statusCode,
|
|
context: err.url || imagePath
|
|
}));
|
|
}).catch({code: 'ENOENT'}, {statusCode: 404}, function (err) {
|
|
return Promise.reject(new errors.NotFoundError({
|
|
message: 'Image not found.',
|
|
code: 'IMAGE_SIZE_URL',
|
|
statusCode: err.statusCode,
|
|
context: err.url || imagePath
|
|
}));
|
|
}).catch(function (err) {
|
|
if (err instanceof errors.GhostError) {
|
|
return Promise.reject(err);
|
|
}
|
|
return Promise.reject(new errors.InternalServerError({
|
|
message: 'Unknown Request error.',
|
|
code: 'IMAGE_SIZE_URL',
|
|
statusCode: err.statusCode,
|
|
context: err.url || imagePath
|
|
}));
|
|
});
|
|
};
|
|
|
|
// Supported formats of https://github.com/image-size/image-size:
|
|
// BMP, GIF, JPEG, PNG, PSD, TIFF, WebP, SVG, ICO
|
|
// ***
|
|
// Takes the url or filepath of the image and reads it form the local
|
|
// file storage.
|
|
// getImageSizeFromFilePath returns an Object like this
|
|
// {
|
|
// height: 50,
|
|
// url: 'http://myblog.com/images/cat.jpg',
|
|
// width: 50
|
|
// };
|
|
// if the image is found and dimensions can be fetched, and rejects with error, if not.
|
|
/**
|
|
* @description read image dimensions from local file storage
|
|
* @param {String} imagePath
|
|
* @returns {object} imageObject or error
|
|
*/
|
|
getImageSizeFromFilePath = function getImageSizeFromFilePath(imagePath) {
|
|
var filePath;
|
|
|
|
imagePath = utils.url.urlFor('image', {image: imagePath}, true);
|
|
|
|
// get the storage readable filePath
|
|
filePath = storageUtils.getLocalFileStoragePath(imagePath);
|
|
|
|
return storage.getStorage()
|
|
.read({path: filePath})
|
|
.then(function readFile(buf) {
|
|
debug('Image fetched (storage):', filePath);
|
|
|
|
return fetchDimensionsFromBuffer({
|
|
buffer: buf,
|
|
// we need to return the URL that's accessible for network requests as this imagePath
|
|
// value will be used as the URL for structured data
|
|
imagePath: imagePath
|
|
});
|
|
}).catch({code: 'ENOENT'}, function (err) {
|
|
return Promise.reject(new errors.NotFoundError({
|
|
message: err.message,
|
|
code: 'IMAGE_SIZE_STORAGE',
|
|
err: err,
|
|
context: filePath,
|
|
errorDetails: {
|
|
originalPath: imagePath,
|
|
reqFilePath: filePath
|
|
}
|
|
}));
|
|
}).catch(function (err) {
|
|
if (err instanceof errors.GhostError) {
|
|
return Promise.reject(err);
|
|
}
|
|
return Promise.reject(new errors.InternalServerError({
|
|
message: err.message,
|
|
code: 'IMAGE_SIZE_STORAGE',
|
|
err: err,
|
|
context: filePath,
|
|
errorDetails: {
|
|
originalPath: imagePath,
|
|
reqFilePath: filePath
|
|
}
|
|
}));
|
|
});
|
|
};
|
|
|
|
module.exports.getImageSizeFromUrl = getImageSizeFromUrl;
|
|
module.exports.getImageSizeFromFilePath = getImageSizeFromFilePath;
|