Skip to content

Commit 2fdf8a3

Browse files
authored
Added timeout when resizing an image (#494)
refs [ENG-827](https://linear.app/tryghost/issue/ENG-827/🐛-crash-on-resizing-animated-gif) Added a configurable timeout when resizing an image to allow the package consumer to handle the case when image processing takes too long. If no timeout is provided, the default value is 0 seconds which means no timeout
1 parent e40636e commit 2fdf8a3

File tree

2 files changed

+45
-5
lines changed

2 files changed

+45
-5
lines changed

packages/image-transform/lib/transform.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const errors = require('@tryghost/errors');
22
const fs = require('fs-extra');
33
const path = require('path');
44

5+
const DEFAULT_PROCESSING_TIMEOUT_SECONDS = 0; // 0 means no timeout
6+
57
/**
68
* Check if this tool can handle any file transformations as Sharp is an optional dependency
79
*/
@@ -58,13 +60,14 @@ const canTransformToFormat = format => [
5860
* https://github.com/lovell/sharp/issues/1360.
5961
*
6062
* Resize an image referenced by the `in` path and write it to the `out` path
61-
* @param {{in, out, width}} options
63+
* @param {{in, out, width, timeout}} options
6264
*/
6365
const unsafeResizeFromPath = (options = {}) => {
6466
return fs.readFile(options.in)
6567
.then((data) => {
6668
return unsafeResizeFromBuffer(data, {
67-
width: options.width
69+
width: options.width,
70+
timeout: options.timeout
6871
});
6972
})
7073
.then((data) => {
@@ -76,7 +79,7 @@ const unsafeResizeFromPath = (options = {}) => {
7679
* Resize an image
7780
*
7881
* @param {Buffer} originalBuffer image to resize
79-
* @param {{width?: number, height?: number, format?: keyof import('sharp').FormatEnum, animated?: boolean, withoutEnlargement?: boolean}} [options]
82+
* @param {{width?: number, height?: number, format?: keyof import('sharp').FormatEnum, animated?: boolean, withoutEnlargement?: boolean, timeout:? number}} [options]
8083
* options.animated defaults to true for file formats where animation is supported (will always maintain animation if possible)
8184
* @returns {Promise<Buffer>} the resizedBuffer
8285
*/
@@ -105,7 +108,8 @@ const unsafeResizeFromBuffer = async (originalBuffer, options = {}) => {
105108
withoutEnlargement: options.withoutEnlargement ?? true
106109
})
107110
// CASE: Automatically remove metadata and rotate based on the orientation.
108-
.rotate();
111+
.rotate()
112+
.timeout({seconds: options.timeout || DEFAULT_PROCESSING_TIMEOUT_SECONDS});
109113

110114
const metadata = await s.metadata();
111115

@@ -162,3 +166,4 @@ module.exports.canTransformToFormat = canTransformToFormat;
162166
module.exports.generateOriginalImageName = generateOriginalImageName;
163167
module.exports.resizeFromPath = makeSafe(unsafeResizeFromPath);
164168
module.exports.resizeFromBuffer = makeSafe(unsafeResizeFromBuffer);
169+
module.exports.DEFAULT_PROCESSING_TIMEOUT_SECONDS = DEFAULT_PROCESSING_TIMEOUT_SECONDS;

packages/image-transform/test/unit/transform.test.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ describe('Transform', function () {
8888
rotate: sinon.stub().returnsThis(),
8989
toBuffer: sinon.stub(),
9090
jpeg: sinon.stub().returnsThis(),
91-
metadata: sinon.stub().returns({format: 'test'})
91+
metadata: sinon.stub().returns({format: 'test'}),
92+
timeout: sinon.stub().returnsThis()
9293
};
9394

9495
sharp = sinon.stub().callsFake(() => {
@@ -157,6 +158,40 @@ describe('Transform', function () {
157158
err.code.should.eql('IMAGE_PROCESSING');
158159
});
159160
});
161+
162+
it('uses the default processing timeout when resizing an image', function () {
163+
sharpInstance.toBuffer.resolves('manipulated');
164+
165+
return transform.resizeFromPath({width: 1000})
166+
.then(() => {
167+
sharpInstance.resize.calledOnce.should.be.true();
168+
sharpInstance.rotate.calledOnce.should.be.true();
169+
sharpInstance.timeout.calledOnce.should.be.true();
170+
171+
sharpInstance.timeout.getCall(0).args[0].should.eql({seconds: transform.DEFAULT_PROCESSING_TIMEOUT_SECONDS});
172+
173+
fs.writeFile.calledOnce.should.be.true();
174+
fs.writeFile.calledWith('manipulated');
175+
});
176+
});
177+
178+
it('uses the provided processing timeout when resizing an image', function () {
179+
sharpInstance.toBuffer.resolves('manipulated');
180+
181+
const timeout = 10;
182+
183+
return transform.resizeFromPath({width: 1000, timeout})
184+
.then(() => {
185+
sharpInstance.resize.calledOnce.should.be.true();
186+
sharpInstance.rotate.calledOnce.should.be.true();
187+
sharpInstance.timeout.calledOnce.should.be.true();
188+
189+
sharpInstance.timeout.getCall(0).args[0].should.eql({seconds: timeout});
190+
191+
fs.writeFile.calledOnce.should.be.true();
192+
fs.writeFile.calledWith('manipulated');
193+
});
194+
});
160195
});
161196

162197
describe('installation', function () {

0 commit comments

Comments
 (0)