From bbae006eb5f6168f0e886755aeaa81e255327bf8 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Tue, 11 Jun 2019 16:25:15 +0100 Subject: [PATCH] Speed up `image-size` utility functions (#10784) no issue - add `probe-image-size` dependency - use `probe-image-size` to fetch partial image data over the network where possible --- core/server/lib/image/image-size.js | 177 +++---- core/test/unit/lib/image/image-size_spec.js | 481 ++++++++++---------- package.json | 2 +- yarn.lock | 2 +- 4 files changed, 338 insertions(+), 324 deletions(-) diff --git a/core/server/lib/image/image-size.js b/core/server/lib/image/image-size.js index 6a221ecf8e..83a8c093e7 100644 --- a/core/server/lib/image/image-size.js +++ b/core/server/lib/image/image-size.js @@ -1,5 +1,6 @@ const debug = require('ghost-ignition').debug('utils:image-size'); const sizeOf = require('image-size'); +const probeSizeOf = require('probe-image-size'); const url = require('url'); const Promise = require('bluebird'); const _ = require('lodash'); @@ -9,50 +10,96 @@ const common = require('../common'); const config = require('../../config'); const storage = require('../../adapters/storage'); const storageUtils = require('../../adapters/storage/utils'); -let getImageSizeFromUrl; -let getImageSizeFromStoragePath; -let getImageSizeFromPath; +const validator = require('../../data/validation').validator; -/** - * @description processes the Buffer result of an image file - * @param {Object} options - * @returns {Object} dimensions - */ -function fetchDimensionsFromBuffer(options) { - const buffer = options.buffer; - const imagePath = options.imagePath; - const imageObject = {}; - let dimensions; +// these are formats supported by image-size but not probe-image-size +const FETCH_ONLY_FORMATS = [ + 'cur', 'icns', 'ico', 'dds' +]; - imageObject.url = imagePath; +const REQUEST_OPTIONS = { + // we need the user-agent, otherwise some https request may fail (e.g. cloudfare) + headers: { + 'User-Agent': 'Mozilla/5.0 Safari/537.36' + }, + timeout: config.get('times:getImageSizeTimeoutInMS') || 10000, + retry: 0, // for `got`, used with image-size + encoding: null +}; - 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); +// processes the Buffer result of an image file using image-size +// returns promise which resolves dimensions +function _imageSizeFromBuffer(buffer) { + return new Promise((resolve, reject) => { + try { + const 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, (w) => { - return w.width; - }).width; - dimensions.height = _.maxBy(dimensions.images, (h) => { - return h.height; - }).height; + // 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, img => img.width).width; + dimensions.height = _.maxBy(dimensions.images, img => img.height).height; + } + + return resolve(dimensions); + } catch (err) { + return reject(err); } + }); +} - imageObject.width = dimensions.width; - imageObject.height = dimensions.height; - - return Promise.resolve(imageObject); - } catch (err) { +// use probe-image-size to download enough of an image to get it's dimensions +// returns promise which resolves dimensions +function _probeImageSizeFromUrl(url) { + // probe-image-size uses `request` npm module which doesn't have our `got` + // override with custom URL validation so it needs duplicating here + if (_.isEmpty(url) || !validator.isURL(url)) { return Promise.reject(new common.errors.InternalServerError({ - code: 'IMAGE_SIZE', - err: err, - context: imagePath + message: 'URL empty or invalid.', + code: 'URL_MISSING_INVALID', + context: url })); } + + return probeSizeOf(url, REQUEST_OPTIONS); +} + +// download full image then use image-size to get it's dimensions +// returns promise which resolves dimensions +function _fetchImageSizeFromUrl(url) { + return request(url, REQUEST_OPTIONS).then((response) => { + return _imageSizeFromBuffer(response.body); + }); +} + +// wrapper for appropriate probe/fetch method for getting image dimensions from a URL +// returns promise which resolves dimensions +function _imageSizeFromUrl(imageUrl) { + return new Promise((resolve, reject) => { + let parsedUrl; + + try { + parsedUrl = url.parse(imageUrl); + } catch (err) { + reject(err); + } + + // 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 + imageUrl = 'http:' + imageUrl; + } + + const extensionMatch = imageUrl.match(/(?:\.)([a-zA-Z]{3,4})(\?|$)/) || []; + const extension = (extensionMatch[1] || '').toLowerCase(); + + if (FETCH_ONLY_FORMATS.includes(extension)) { + return resolve(_fetchImageSizeFromUrl(imageUrl)); + } else { + return resolve(_probeImageSizeFromUrl(imageUrl)); + } + }); } // Supported formats of https://github.com/image-size/image-size: @@ -78,11 +125,7 @@ function fetchDimensionsFromBuffer(options) { * @param {String} imagePath as URL * @returns {Promise} imageObject or error */ -getImageSizeFromUrl = (imagePath) => { - let requestOptions; - let parsedUrl; - let timeout = config.get('times:getImageSizeTimeoutInMS') || 10000; - +const getImageSizeFromUrl = (imagePath) => { if (storageUtils.isLocalImage(imagePath)) { // don't make a request for a locally stored image return getImageSizeFromStoragePath(imagePath); @@ -93,36 +136,16 @@ getImageSizeFromUrl = (imagePath) => { imagePath = urlService.utils.urlJoin(urlService.utils.urlFor('home', true), urlService.utils.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 Safari/537.36' - }, - timeout: timeout, - encoding: null - }; - return request( - imagePath, - requestOptions - ).then((response) => { + return _imageSizeFromUrl(imagePath).then((dimensions) => { 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 - }); + return { + url: imagePath, + width: dimensions.width, + height: dimensions.height + }; }).catch({code: 'URL_MISSING_INVALID'}, (err) => { return Promise.reject(new common.errors.InternalServerError({ message: err.message, @@ -176,7 +199,7 @@ getImageSizeFromUrl = (imagePath) => { * @param {String} imagePath * @returns {object} imageObject or error */ -getImageSizeFromStoragePath = (imagePath) => { +const getImageSizeFromStoragePath = (imagePath) => { let filePath; imagePath = urlService.utils.urlFor('image', {image: imagePath}, true); @@ -188,14 +211,16 @@ getImageSizeFromStoragePath = (imagePath) => { .read({path: filePath}) .then((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'}, (err) => { + return _imageSizeFromBuffer(buf); + }) + .then((dimensions) => { + return { + url: imagePath, + width: dimensions.width, + height: dimensions.height + }; + }) + .catch({code: 'ENOENT'}, (err) => { return Promise.reject(new common.errors.NotFoundError({ message: err.message, code: 'IMAGE_SIZE_STORAGE', @@ -233,7 +258,7 @@ getImageSizeFromStoragePath = (imagePath) => { * @returns {Promise} getImageDimensions * @description Takes a file path and returns width and height. */ -getImageSizeFromPath = (path) => { +const getImageSizeFromPath = (path) => { return new Promise(function getSize(resolve, reject) { let dimensions; diff --git a/core/test/unit/lib/image/image-size_spec.js b/core/test/unit/lib/image/image-size_spec.js index a9c09bd3f0..020e615df3 100644 --- a/core/test/unit/lib/image/image-size_spec.js +++ b/core/test/unit/lib/image/image-size_spec.js @@ -1,4 +1,4 @@ -var should = require('should'), +const should = require('should'), sinon = require('sinon'), rewire = require('rewire'), nock = require('nock'), @@ -6,26 +6,34 @@ var should = require('should'), configUtils = require('../../../utils/configUtils'), urlService = require('../../../../server/services/url'), common = require('../../../../server/lib/common'), - storage = require('../../../../server/adapters/storage'), - - // Stuff we are testing - imageSize = rewire('../../../../server/lib/image/image-size'); + storage = require('../../../../server/adapters/storage'); describe('lib/image: image size', function () { - var sizeOfStub, - result, - requestMock, - secondRequestMock, - originalStoragePath; + let imageSize; + let sizeOf; + let sizeOfSpy; + let probeSizeOf; + let probeSizeOfSpy; + let originalStoragePath; + + // use a 1x1 gif in nock responses because it's really small and easy to work with + const GIF1x1 = Buffer.from('R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', 'base64'); beforeEach(function () { + imageSize = rewire('../../../../server/lib/image/image-size'); + + sizeOf = imageSize.__get__('sizeOf'); + sizeOfSpy = sinon.spy(sizeOf); + + probeSizeOf = imageSize.__get__('probeSizeOf'); + probeSizeOfSpy = sinon.spy(probeSizeOf); + originalStoragePath = storage.getStorage().storagePath; }); afterEach(function () { sinon.restore(); configUtils.restore(); - imageSize = rewire('../../../../server/lib/image/image-size'); storage.getStorage().storagePath = originalStoragePath; }); @@ -35,281 +43,234 @@ describe('lib/image: image size', function () { }); describe('getImageSizeFromUrl', function () { - it('[success] should return image dimensions with http request', function (done) { - var url = 'http://img.stockfresh.com/files/f/feedough/x/11/1540353_20925115.jpg', - expectedImageObject = - { - height: 50, - url: 'http://img.stockfresh.com/files/f/feedough/x/11/1540353_20925115.jpg', - width: 50 - }; + it('[success] should return image dimensions from probe request for probe-supported extension', function (done) { + const url = 'http://img.stockfresh.com/files/f/feedough/x/11/1540353_20925115.jpg'; + const expectedImageObject = { + height: 1, + url: 'http://img.stockfresh.com/files/f/feedough/x/11/1540353_20925115.jpg', + width: 1 + }; - requestMock = nock('http://img.stockfresh.com') + const requestMock = nock('http://img.stockfresh.com') .get('/files/f/feedough/x/11/1540353_20925115.jpg') - .reply(200); + .reply(200, GIF1x1); - sizeOfStub = sinon.stub(); - sizeOfStub.returns({width: 50, height: 50, type: 'jpg'}); - imageSize.__set__('sizeOf', sizeOfStub); + imageSize.getImageSizeFromUrl(url).then(function (res) { + probeSizeOfSpy.should.have.been.called; + sizeOfSpy.should.not.have.been.called; - result = imageSize.getImageSizeFromUrl(url).then(function (res) { requestMock.isDone().should.be.true(); should.exist(res); - should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); - should.exist(res.height); res.height.should.be.equal(expectedImageObject.height); - should.exist(res.url); res.url.should.be.equal(expectedImageObject.url); done(); }).catch(done); }); - it('[success] should return image dimensions with https request', function (done) { - var url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256', - expectedImageObject = - { - height: 256, - url: 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256', - width: 256 - }; + it('[success] should return image dimensions from fetch request for non-probe-supported extension', function (done) { + const url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.ico'; + const expectedImageObject = { + height: 1, + url: 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.ico', + width: 1 + }; - requestMock = nock('https://static.wixstatic.com') - .get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256') - .reply(200, { - body: '' - }); + const requestMock = nock('https://static.wixstatic.com') + .get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.ico') + .reply(200, GIF1x1); - sizeOfStub = sinon.stub(); - sizeOfStub.returns({width: 256, height: 256, type: 'png'}); - imageSize.__set__('sizeOf', sizeOfStub); + imageSize.getImageSizeFromUrl(url).then(function (res) { + sizeOfSpy.should.have.been.called; + probeSizeOfSpy.should.not.have.been.called; - result = imageSize.getImageSizeFromUrl(url).then(function (res) { requestMock.isDone().should.be.true(); should.exist(res); - should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); - should.exist(res.height); res.height.should.be.equal(expectedImageObject.height); - should.exist(res.url); res.url.should.be.equal(expectedImageObject.url); done(); }).catch(done); }); it('[success] should return image dimensions when no image extension given', function (done) { - // This test is mocked, but works with this specific example. - // You can comment out the mocks and the test should still pass. - var url = 'https://www.zomato.com/logo/18163505/minilogo', - expectedImageObject = - { - height: 15, - url: 'https://www.zomato.com/logo/18163505/minilogo', - width: 104 - }; + const url = 'https://www.zomato.com/logo/18163505/minilogo'; + const expectedImageObject = { + height: 1, + url: 'https://www.zomato.com/logo/18163505/minilogo', + width: 1 + }; - requestMock = nock('https://www.zomato.com') - .matchHeader('User-Agent', /Mozilla\/.*Safari\/.*/) + const requestMock = nock('https://www.zomato.com') .get('/logo/18163505/minilogo') - .reply(200, { - body: '' - }); + .reply(200, GIF1x1); - sizeOfStub = sinon.stub(); - sizeOfStub.returns({width: 104, height: 15, type: 'png'}); - imageSize.__set__('sizeOf', sizeOfStub); - - result = imageSize.getImageSizeFromUrl(url).then(function (res) { + imageSize.getImageSizeFromUrl(url).then(function (res) { + probeSizeOfSpy.should.have.been.called; requestMock.isDone().should.be.true(); should.exist(res); - should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); - should.exist(res.height); res.height.should.be.equal(expectedImageObject.height); - should.exist(res.url); res.url.should.be.equal(expectedImageObject.url); done(); }).catch(done); }); it('[success] should returns largest image value for .ico files', function (done) { - var url = 'https://super-website.com/media/icon.ico', - expectedImageObject = - { - height: 48, - url: 'https://super-website.com/media/icon.ico', - width: 48 - }; + const url = 'https://super-website.com/media/icon.ico'; + const expectedImageObject = { + height: 64, + url: 'https://super-website.com/media/icon.ico', + width: 64 + }; - requestMock = nock('https://super-website.com') + const requestMock = nock('https://super-website.com') .get('/media/icon.ico') - .reply(200, { - body: '' - }); + .replyWithFile(200, path.join(__dirname, '/../../../utils/fixtures/images/favicon_multi_sizes.ico')); - sizeOfStub = sinon.stub(); - sizeOfStub.returns({ - width: 32, - height: 32, - type: 'ico', - images: [ - {width: 48, height: 48}, - {width: 32, height: 32}, - {width: 16, height: 16} - ] - }); - imageSize.__set__('sizeOf', sizeOfStub); - - result = imageSize.getImageSizeFromUrl(url).then(function (res) { + imageSize.getImageSizeFromUrl(url).then(function (res) { requestMock.isDone().should.be.true(); should.exist(res); - should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); - should.exist(res.height); res.height.should.be.equal(expectedImageObject.height); - should.exist(res.url); res.url.should.be.equal(expectedImageObject.url); done(); }).catch(done); }); it('[success] should return image dimensions asset path images', function (done) { - var url = '/assets/img/logo.png?v=d30c3d1e41', - urlForStub, - urlGetSubdirStub, - expectedImageObject = - { - height: 100, - url: 'http://myblog.com/assets/img/logo.png?v=d30c3d1e41', - width: 100 - }; + const url = '/assets/img/logo.png?v=d30c3d1e41'; + const expectedImageObject = { + height: 1, + url: 'http://myblog.com/assets/img/logo.png?v=d30c3d1e41', + width: 1 + }; - urlForStub = sinon.stub(urlService.utils, 'urlFor'); + const urlForStub = sinon.stub(urlService.utils, 'urlFor'); urlForStub.withArgs('home').returns('http://myblog.com/'); - urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); + const urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); urlGetSubdirStub.returns(''); - requestMock = nock('http://myblog.com') + const requestMock = nock('http://myblog.com') .get('/assets/img/logo.png?v=d30c3d1e41') - .reply(200, { - body: '' - }); + .reply(200, GIF1x1); - sizeOfStub = sinon.stub(); - sizeOfStub.returns({width: 100, height: 100, type: 'svg'}); - imageSize.__set__('sizeOf', sizeOfStub); - - result = imageSize.getImageSizeFromUrl(url).then(function (res) { + imageSize.getImageSizeFromUrl(url).then(function (res) { + probeSizeOfSpy.should.have.been.called; requestMock.isDone().should.be.true(); should.exist(res); - should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); - should.exist(res.height); res.height.should.be.equal(expectedImageObject.height); - should.exist(res.url); res.url.should.be.equal(expectedImageObject.url); done(); }).catch(done); }); it('[success] should return image dimensions for gravatar images request', function (done) { - var url = '//www.gravatar.com/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=250&d=mm&r=x', - expectedImageObject = - { - height: 250, - url: '//www.gravatar.com/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=250&d=mm&r=x', - width: 250 - }; + const url = '//www.gravatar.com/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=250&d=mm&r=x'; + const expectedImageObject = { + height: 1, + url: '//www.gravatar.com/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=250&d=mm&r=x', + width: 1 + }; - requestMock = nock('http://www.gravatar.com') + const requestMock = nock('http://www.gravatar.com') .get('/avatar/ef6dcde5c99bb8f685dd451ccc3e050a?s=250&d=mm&r=x') - .reply(200, { - body: '' - }); + .reply(200, GIF1x1); - sizeOfStub = sinon.stub(); - sizeOfStub.returns({width: 250, height: 250, type: 'jpg'}); - imageSize.__set__('sizeOf', sizeOfStub); - - result = imageSize.getImageSizeFromUrl(url).then(function (res) { + imageSize.getImageSizeFromUrl(url).then(function (res) { + probeSizeOfSpy.should.have.been.called; requestMock.isDone().should.be.true(); should.exist(res); - should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); - should.exist(res.height); res.height.should.be.equal(expectedImageObject.height); - should.exist(res.url); res.url.should.be.equal(expectedImageObject.url); done(); }).catch(done); }); - it('[success] can handle redirect', function (done) { - var url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg', - expectedImageObject = - { - height: 100, - url: 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg', - width: 100 - }; + it('[success] can handle redirect (probe-image-size)', function (done) { + const url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg'; + const expectedImageObject = { + height: 1, + url: 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg', + width: 1 + }; - requestMock = nock('http://noimagehere.com') + const requestMock = nock('http://noimagehere.com') .get('/files/f/feedough/x/11/1540353_20925115.jpg') - .reply(301, { - body: '' - }, - { - location: 'http://someredirectedurl.com/files/f/feedough/x/11/1540353_20925115.jpg' - }); - - secondRequestMock = nock('http://someredirectedurl.com') - .get('/files/f/feedough/x/11/1540353_20925115.jpg') - .reply(200, { - body: '' + .reply(301, null, { + location: 'http://someredirectedurl.com/files/f/feedough/x/11/1540353_20925115.jpg' }); - sizeOfStub.returns({width: 100, height: 100, type: 'jpg'}); - imageSize.__set__('sizeOf', sizeOfStub); + const secondRequestMock = nock('http://someredirectedurl.com') + .get('/files/f/feedough/x/11/1540353_20925115.jpg') + .reply(200, GIF1x1); - result = imageSize.getImageSizeFromUrl(url).then(function (res) { + imageSize.getImageSizeFromUrl(url).then(function (res) { + probeSizeOfSpy.should.have.been.called; + requestMock.isDone().should.be.true(); + secondRequestMock.isDone().should.be.true(); + should.exist(res); + res.width.should.be.equal(expectedImageObject.width); + res.height.should.be.equal(expectedImageObject.height); + res.url.should.be.equal(expectedImageObject.url); + done(); + }).catch(done); + }); + + it('[success] can handle redirect (image-size)', function (done) { + const url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.gif'; + const expectedImageObject = { + height: 1, + url: 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.gif', + width: 1 + }; + + const requestMock = nock('http://noimagehere.com') + .get('/files/f/feedough/x/11/1540353_20925115.gif') + .reply(301, null, { + location: 'http://someredirectedurl.com/files/f/feedough/x/11/1540353_20925115.gif' + }); + + const secondRequestMock = nock('http://someredirectedurl.com') + .get('/files/f/feedough/x/11/1540353_20925115.gif') + .reply(200, GIF1x1); + + imageSize.getImageSizeFromUrl(url).then(function (res) { + sizeOfSpy.should.have.been.called; requestMock.isDone().should.be.true(); secondRequestMock.isDone().should.be.true(); should.exist(res); - should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); - should.exist(res.height); res.height.should.be.equal(expectedImageObject.height); - should.exist(res.url); res.url.should.be.equal(expectedImageObject.url); done(); }).catch(done); }); it('[success] should switch to local file storage if available', function (done) { - var url = '/content/images/favicon.png', - urlForStub, - urlGetSubdirStub, - expectedImageObject = - { - height: 100, - url: 'http://myblog.com/content/images/favicon.png', - width: 100 - }; + const url = '/content/images/favicon.png'; + const expectedImageObject = { + height: 100, + url: 'http://myblog.com/content/images/favicon.png', + width: 100 + }; storage.getStorage().storagePath = path.join(__dirname, '../../../../test/utils/fixtures/images/'); - urlForStub = sinon.stub(urlService.utils, 'urlFor'); + const urlForStub = sinon.stub(urlService.utils, 'urlFor'); urlForStub.withArgs('image').returns('http://myblog.com/content/images/favicon.png'); urlForStub.withArgs('home').returns('http://myblog.com/'); - urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); + const urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); urlGetSubdirStub.returns(''); - requestMock = nock('http://myblog.com') + const requestMock = nock('http://myblog.com') .get('/content/images/favicon.png') .reply(200, { body: '' }); - result = imageSize.getImageSizeFromUrl(url).then(function (res) { + imageSize.getImageSizeFromUrl(url).then(function (res) { requestMock.isDone().should.be.false(); should.exist(res); should.exist(res.width); @@ -322,15 +283,34 @@ describe('lib/image: image size', function () { }).catch(done); }); - it('[failure] can handle an error with statuscode not 200', function (done) { - var url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg'; + it('[failure] can handle an error with statuscode not 200 (probe-image-size)', function (done) { + const url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg'; - requestMock = nock('http://noimagehere.com') + const requestMock = nock('http://noimagehere.com') .get('/files/f/feedough/x/11/1540353_20925115.jpg') .reply(404); - result = imageSize.getImageSizeFromUrl(url) + imageSize.getImageSizeFromUrl(url) .catch(function (err) { + probeSizeOfSpy.should.have.been.called; + requestMock.isDone().should.be.true(); + should.exist(err); + err.errorType.should.be.equal('NotFoundError'); + err.message.should.be.equal('Image not found.'); + done(); + }); + }); + + it('[failure] can handle an error with statuscode not 200 (image-size)', function (done) { + const url = 'http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.gif'; + + const requestMock = nock('http://noimagehere.com') + .get('/files/f/feedough/x/11/1540353_20925115.gif') + .reply(404); + + imageSize.getImageSizeFromUrl(url) + .catch(function (err) { + sizeOfSpy.should.have.been.called; requestMock.isDone().should.be.true(); should.exist(err); err.errorType.should.be.equal('NotFoundError'); @@ -340,9 +320,9 @@ describe('lib/image: image size', function () { }); it('[failure] handles invalid URL', function (done) { - var url = 'Not-a-valid-url'; + const url = 'Not-a-valid-url'; - result = imageSize.getImageSizeFromUrl(url) + imageSize.getImageSizeFromUrl(url) .catch(function (err) { should.exist(err); err.errorType.should.be.equal('InternalServerError'); @@ -352,15 +332,15 @@ describe('lib/image: image size', function () { }); it('[failure] will timeout', function (done) { - var url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256'; + const url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256'; - requestMock = nock('https://static.wixstatic.com') + const requestMock = nock('https://static.wixstatic.com') .get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256') .socketDelay(11) .reply(408); configUtils.set('times:getImageSizeTimeoutInMS', 10); - result = imageSize.getImageSizeFromUrl(url) + imageSize.getImageSizeFromUrl(url) .catch(function (err) { requestMock.isDone().should.be.true(); should.exist(err); @@ -370,20 +350,42 @@ describe('lib/image: image size', function () { }); }); - it('[failure] returns error if \`image-size`\ module throws error', function (done) { - var url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256'; + it('[failure] returns error if \`probe-image-size`\ module throws error', function (done) { + const url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg'; - requestMock = nock('https://static.wixstatic.com') - .get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256') + const probeSizeOfStub = sinon.stub(); + probeSizeOfStub.throws({error: 'probe-image-size could not find dimensions'}); + imageSize.__set__('probeSizeOf', probeSizeOfStub); + + imageSize.getImageSizeFromUrl(url) + .then(() => { + true.should.be.false('succeeded when expecting failure'); + }) + .catch(function (err) { + should.exist(err); + err.errorType.should.be.equal('InternalServerError'); + err.error.should.be.equal('probe-image-size could not find dimensions'); + done(); + }); + }); + + it('[failure] returns error if \`image-size`\ module throws error', function (done) { + const url = 'https://static.wixstatic.com/media/355241_d31358572a2542c5a44738ddcb59e7ea.ico'; + + const requestMock = nock('https://static.wixstatic.com') + .get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.ico') .reply(200, { body: '' }); - sizeOfStub = sinon.stub(); + const sizeOfStub = sinon.stub(); sizeOfStub.throws({error: 'image-size could not find dimensions'}); imageSize.__set__('sizeOf', sizeOfStub); - result = imageSize.getImageSizeFromUrl(url) + imageSize.getImageSizeFromUrl(url) + .then(() => { + true.should.be.false('succeeded when expecting failure'); + }) .catch(function (err) { requestMock.isDone().should.be.true(); should.exist(err); @@ -394,13 +396,13 @@ describe('lib/image: image size', function () { }); it('[failure] returns error if request errors', function (done) { - var url = 'https://notarealwebsite.com/images/notapicture.jpg'; + const url = 'https://notarealwebsite.com/images/notapicture.jpg'; - requestMock = nock('https://notarealwebsite.com') + const requestMock = nock('https://notarealwebsite.com') .get('/images/notapicture.jpg') .reply(500, {message: 'something awful happened', code: 'AWFUL_ERROR'}); - result = imageSize.getImageSizeFromUrl(url) + imageSize.getImageSizeFromUrl(url) .catch(function (err) { requestMock.isDone().should.be.true(); should.exist(err); @@ -413,24 +415,21 @@ describe('lib/image: image size', function () { describe('getImageSizeFromStoragePath', function () { it('[success] should return image dimensions for locally stored images', function (done) { - var url = '/content/images/ghost-logo.png', - urlForStub, - urlGetSubdirStub, - expectedImageObject = - { - height: 257, - url: 'http://myblog.com/content/images/ghost-logo.png', - width: 800 - }; + const url = '/content/images/ghost-logo.png'; + const expectedImageObject = { + height: 257, + url: 'http://myblog.com/content/images/ghost-logo.png', + width: 800 + }; storage.getStorage().storagePath = path.join(__dirname, '../../../../test/utils/fixtures/images/'); - urlForStub = sinon.stub(urlService.utils, 'urlFor'); + const urlForStub = sinon.stub(urlService.utils, 'urlFor'); urlForStub.withArgs('image').returns('http://myblog.com/content/images/ghost-logo.png'); urlForStub.withArgs('home').returns('http://myblog.com/'); - urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); + const urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); urlGetSubdirStub.returns(''); - result = imageSize.getImageSizeFromStoragePath(url).then(function (res) { + imageSize.getImageSizeFromStoragePath(url).then(function (res) { should.exist(res); should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); @@ -443,24 +442,21 @@ describe('lib/image: image size', function () { }); it('[success] should return image dimensions for locally stored images with subdirectory', function (done) { - var url = '/content/images/favicon_too_large.png', - urlForStub, - urlGetSubdirStub, - expectedImageObject = - { - height: 1010, - url: 'http://myblog.com/blog/content/images/favicon_too_large.png', - width: 1010 - }; + const url = '/content/images/favicon_too_large.png'; + const expectedImageObject = { + height: 1010, + url: 'http://myblog.com/blog/content/images/favicon_too_large.png', + width: 1010 + }; storage.getStorage().storagePath = path.join(__dirname, '../../../../test/utils/fixtures/images/'); - urlForStub = sinon.stub(urlService.utils, 'urlFor'); + const urlForStub = sinon.stub(urlService.utils, 'urlFor'); urlForStub.withArgs('image').returns('http://myblog.com/blog/content/images/favicon_too_large.png'); urlForStub.withArgs('home').returns('http://myblog.com/'); - urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); + const urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); urlGetSubdirStub.returns('/blog'); - result = imageSize.getImageSizeFromStoragePath(url).then(function (res) { + imageSize.getImageSizeFromStoragePath(url).then(function (res) { should.exist(res); should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); @@ -473,24 +469,21 @@ describe('lib/image: image size', function () { }); it('[success] should return largest image dimensions for locally stored .ico image', function (done) { - var url = 'http://myblog.com/content/images/favicon_multi_sizes.ico', - urlForStub, - urlGetSubdirStub, - expectedImageObject = - { - height: 64, - url: 'http://myblog.com/content/images/favicon_multi_sizes.ico', - width: 64 - }; + const url = 'http://myblog.com/content/images/favicon_multi_sizes.ico'; + const expectedImageObject = { + height: 64, + url: 'http://myblog.com/content/images/favicon_multi_sizes.ico', + width: 64 + }; storage.getStorage().storagePath = path.join(__dirname, '../../../../test/utils/fixtures/images/'); - urlForStub = sinon.stub(urlService.utils, 'urlFor'); + const urlForStub = sinon.stub(urlService.utils, 'urlFor'); urlForStub.withArgs('image').returns('http://myblog.com/content/images/favicon_multi_sizes.ico'); urlForStub.withArgs('home').returns('http://myblog.com/'); - urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); + const urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); urlGetSubdirStub.returns(''); - result = imageSize.getImageSizeFromStoragePath(url).then(function (res) { + imageSize.getImageSizeFromStoragePath(url).then(function (res) { should.exist(res); should.exist(res.width); res.width.should.be.equal(expectedImageObject.width); @@ -503,18 +496,16 @@ describe('lib/image: image size', function () { }); it('[failure] returns error if storage adapter errors', function (done) { - var url = '/content/images/not-existing-image.png', - urlForStub, - urlGetSubdirStub; + const url = '/content/images/not-existing-image.png'; storage.getStorage().storagePath = path.join(__dirname, '../../../../test/utils/fixtures/images/'); - urlForStub = sinon.stub(urlService.utils, 'urlFor'); + const urlForStub = sinon.stub(urlService.utils, 'urlFor'); urlForStub.withArgs('image').returns('http://myblog.com/content/images/not-existing-image.png'); urlForStub.withArgs('home').returns('http://myblog.com/'); - urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); + const urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); urlGetSubdirStub.returns(''); - result = imageSize.getImageSizeFromStoragePath(url) + imageSize.getImageSizeFromStoragePath(url) .catch(function (err) { should.exist(err); (err instanceof common.errors.NotFoundError).should.eql(true); @@ -523,22 +514,20 @@ describe('lib/image: image size', function () { }); it('[failure] returns error if \`image-size`\ module throws error', function (done) { - var url = '/content/images/ghost-logo.pngx', - urlForStub, - urlGetSubdirStub; + const url = '/content/images/ghost-logo.pngx'; - sizeOfStub = sinon.stub(); + const sizeOfStub = sinon.stub(); sizeOfStub.throws({error: 'image-size could not find dimensions'}); imageSize.__set__('sizeOf', sizeOfStub); storage.getStorage().storagePath = path.join(__dirname, '../../../../test/utils/fixtures/images/'); - urlForStub = sinon.stub(urlService.utils, 'urlFor'); + const urlForStub = sinon.stub(urlService.utils, 'urlFor'); urlForStub.withArgs('image').returns('http://myblog.com/content/images/ghost-logo.pngx'); urlForStub.withArgs('home').returns('http://myblog.com/'); - urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); + const urlGetSubdirStub = sinon.stub(urlService.utils, 'getSubdir'); urlGetSubdirStub.returns(''); - result = imageSize.getImageSizeFromStoragePath(url) + imageSize.getImageSizeFromStoragePath(url) .catch(function (err) { should.exist(err); err.error.should.be.equal('image-size could not find dimensions'); diff --git a/package.json b/package.json index a66dac7b79..3dfedc926a 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", "path-match": "1.2.4", - "probe-image-size": "^4.0.0", + "probe-image-size": "4.0.0", "rss": "1.2.2", "sanitize-html": "1.20.0", "semver": "5.6.0", diff --git a/yarn.lock b/yarn.lock index 84fd7d6202..c0e3318f9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5701,7 +5701,7 @@ prettyjson@^1.1.3: colors "^1.1.2" minimist "^1.2.0" -probe-image-size@^4.0.0: +probe-image-size@4.0.0, probe-image-size@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-4.0.0.tgz#d35b71759e834bcf580ea9f18ec8b9265c0977eb" integrity sha512-nm7RvWUxps+2+jZKNLkd04mNapXNariS6G5WIEVzvAqjx7EUuKcY1Dp3e6oUK7GLwzJ+3gbSbPLFAASHFQrPcQ==