Skip to content

Commit b653246

Browse files
authored
Merge pull request #161 from wchaws/dev
feat: support parameter store to bypass large gif processing
2 parents d1036a9 + c26d1f7 commit b653246

File tree

8 files changed

+86
-8
lines changed

8 files changed

+86
-8
lines changed

source/constructs/cdk.context.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"//enable_cloudfront": true,
1313
"secret_arn": "arn:aws:secretsmanager:<region>:<account-id-number>:secret:<secret-name>-<random-6-characters>",
1414
"stack_tags":{"key1":"value1", "key2":"value2"},
15+
"config_json_parameter_name": "<parameter-name>",
1516
"ecs_desired_count": 10,
1617
"env": {
1718
"SHARP_QUEUE_LIMIT": "1"

source/constructs/lib/ecs-image-handler.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as ecs from '@aws-cdk/aws-ecs';
77
import * as ecsPatterns from '@aws-cdk/aws-ecs-patterns';
88
import * as iam from '@aws-cdk/aws-iam';
99
import * as s3 from '@aws-cdk/aws-s3';
10+
import * as ssm from '@aws-cdk/aws-ssm';
1011
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
1112
import { Aws, CfnOutput, Construct, Duration, Stack } from '@aws-cdk/core';
1213

@@ -34,6 +35,7 @@ export class ECSImageHandler extends Construct {
3435

3536
this.cfnOutput('StyleConfig', table.tableName, 'The DynamoDB table for processing style');
3637

38+
const configJsonParameter = this.getConfigJsonParameter();
3739
const vpc = getOrCreateVpc(this);
3840
const taskSubnets = getTaskSubnets(this, vpc);
3941
const albFargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', {
@@ -59,12 +61,15 @@ export class ECSImageHandler extends Construct {
5961
SRC_BUCKET: buckets[0].bucketName,
6062
STYLE_TABLE_NAME: table.tableName,
6163
SECRET_NAME: secret?.secretArn,
64+
CONFIG_JSON_PARAMETER_NAME: configJsonParameter.parameterName,
6265
}, scope.node.tryGetContext('env')),
6366
},
6467
});
6568
albFargateService.targetGroup.configureHealthCheck({
6669
path: '/',
6770
healthyThresholdCount: 3,
71+
timeout: Duration.seconds(10),
72+
interval: Duration.seconds(60),
6873
});
6974
albFargateService.service.autoScaleTaskCount({
7075
minCapacity: 8,
@@ -76,6 +81,8 @@ export class ECSImageHandler extends Construct {
7681

7782
const taskRole = albFargateService.taskDefinition.taskRole;
7883
table.grantReadData(taskRole);
84+
configJsonParameter.grantRead(taskRole);
85+
7986
for (const bkt of buckets) {
8087

8188
taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
@@ -156,6 +163,15 @@ export class ECSImageHandler extends Construct {
156163
o.overrideLogicalId(id);
157164
return o;
158165
}
166+
167+
private getConfigJsonParameter() {
168+
const name = this.node.tryGetContext('config_json_parameter_name');
169+
if (name) {
170+
return ssm.StringParameter.fromStringParameterName(this, 'ConfigJsonParameter', name);
171+
} else {
172+
throw new Error('Missing "config_json_parameter_name" in cdk.context.json');
173+
}
174+
}
159175
}
160176

161177
function getOrCreateVpc(scope: Construct): ec2.IVpc {

source/constructs/package.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

source/constructs/test/ecs-image-handler.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ test('Snapshot', () => {
77
context: {
88
buckets: ['bucket-0'],
99
secret_arn: 'arn:aws:secretsmanager:us-east-9:123456789012:secret:test-aaabbb',
10+
config_json_parameter_name: 'config_json_parameter_name',
1011
},
1112
});
1213
const stack = new ECSImageHandlerStack(app, 'test');

source/new-image-handler/src/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ export interface IConfig {
77
autoWebp: boolean;
88
secretName: string;
99
sharpQueueLimit: number;
10+
configJsonParameterName: string;
1011
}
1112

1213
const conf: IConfig = {
1314
port: 8080,
14-
region: process.env.REGION ?? 'us-west-2',
15+
region: process.env.REGION ?? process.env.AWS_REGION ?? 'us-west-2',
1516
isProd: process.env.NODE_ENV === 'production',
1617
srcBucket: process.env.BUCKET || process.env.SRC_BUCKET || 'sih-input',
1718
styleTableName: process.env.STYLE_TABLE_NAME || 'style-table-name',
1819
autoWebp: ['yes', '1', 'true'].includes((process.env.AUTO_WEBP ?? '').toLowerCase()),
1920
secretName: process.env.SECRET_NAME ?? 'X-Client-Authorization',
2021
sharpQueueLimit: Number.parseInt(process.env.SHARP_QUEUE_LIMIT ?? '1', 10),
22+
configJsonParameterName: process.env.CONFIG_JSON_PARAMETER_NAME ?? '',
2123
};
2224

2325
export default conf;

source/new-image-handler/src/default.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ const PROCESSOR_MAP: { [key: string]: IProcessor } = {
1414
[VideoProcessor.getInstance().name]: VideoProcessor.getInstance(),
1515
};
1616

17+
export function setMaxGifSizeMB(value: number) {
18+
ImageProcessor.getInstance().setMaxGifSizeMB(value);
19+
}
20+
21+
export function setMaxGifPages(value: number) {
22+
ImageProcessor.getInstance().setMaxGifPages(value);
23+
}
24+
1725
export function getProcessor(name: string): IProcessor {
1826
const processor = PROCESSOR_MAP[name];
1927
if (!processor) {

source/new-image-handler/src/index.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as S3 from 'aws-sdk/clients/s3';
22
import * as SecretsManager from 'aws-sdk/clients/secretsmanager';
3+
import * as SSM from 'aws-sdk/clients/ssm';
34
import * as HttpErrors from 'http-errors';
45
import * as Koa from 'koa'; // http://koajs.cn
56
import * as bodyParser from 'koa-bodyparser';
@@ -8,9 +9,13 @@ import * as Router from 'koa-router';
89
import * as sharp from 'sharp';
910
import config from './config';
1011
import debug from './debug';
11-
import { bufferStore, getProcessor, parseRequest } from './default';
12+
import { bufferStore, getProcessor, parseRequest, setMaxGifSizeMB, setMaxGifPages } from './default';
13+
import * as is from './is';
1214
import { IHttpHeaders, InvalidArgument } from './processor';
1315

16+
const ssm = new SSM({ region: config.region });
17+
const smclient = new SecretsManager({ region: config.region });
18+
1419
const DefaultBufferStore = bufferStore();
1520
const app = new Koa();
1621
const router = new Router();
@@ -47,6 +52,12 @@ router.post('/images', async (ctx) => {
4752

4853
router.get(['/', '/ping'], async (ctx) => {
4954
ctx.body = 'ok';
55+
56+
try {
57+
await setMaxGifLimit();
58+
} catch (err: any) {
59+
console.error(err);
60+
}
5061
});
5162

5263
router.get(['/debug', '/_debug'], async (ctx) => {
@@ -162,11 +173,6 @@ function bypass() {
162173
throw new HttpErrors[403]('Please visit s3 directly');
163174
}
164175

165-
// Create a Secrets Manager client
166-
const smclient = new SecretsManager({
167-
region: config.region,
168-
});
169-
170176
async function getSecretFromSecretsManager() {
171177
// Load the AWS SDK
172178
const secretName = config.secretName;
@@ -178,4 +184,21 @@ async function getHeaderFromSecretsManager() {
178184
const secretString = secret.SecretString!;
179185
const keypair = JSON.parse(secretString);
180186
return keypair['X-Client-Authorization'];
187+
}
188+
189+
async function setMaxGifLimit() {
190+
if (config.configJsonParameterName) {
191+
const data = await ssm.getParameter({ Name: config.configJsonParameterName }).promise();
192+
if (data.Parameter) {
193+
const configJson = JSON.parse(data.Parameter.Value ?? '{}');
194+
const maxGifSizeMB = configJson.max_gif_size_mb;
195+
if (is.number(maxGifSizeMB)) {
196+
setMaxGifSizeMB(maxGifSizeMB);
197+
}
198+
const maxGifPages = configJson.max_gif_pages;
199+
if (is.number(maxGifPages)) {
200+
setMaxGifPages(maxGifPages);
201+
}
202+
}
203+
}
181204
}

source/new-image-handler/src/processor/image/index.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,29 @@ export class ImageProcessor implements IProcessor {
4242
}
4343
private static _instance: ImageProcessor;
4444
private readonly _actions: { [name: string]: IAction } = {};
45+
private _maxGifSizeMB: number = 5;
46+
private _maxGifPages: number = 100;
4547

4648
public readonly name: string = 'image';
4749

4850
private constructor() { }
4951

52+
public setMaxGifSizeMB(value: number) {
53+
if (value > 0) {
54+
this._maxGifSizeMB = value;
55+
} else {
56+
console.warn(`Max gif size must > 0, but the value is ${value}`);
57+
}
58+
}
59+
60+
public setMaxGifPages(value: number) {
61+
if (value > 0) {
62+
this._maxGifPages = value;
63+
} else {
64+
console.warn(`Max gif pages must > 0, but the value is ${value}`);
65+
}
66+
}
67+
5068
public async newContext(uri: string, actions: string[], bufferStore: IBufferStore): Promise<IImageContext> {
5169
const ctx: IProcessContext = {
5270
uri,
@@ -94,6 +112,14 @@ export class ImageProcessor implements IProcessor {
94112
}
95113
if ('gif' === metadata.format) {
96114
image.gif({ effort: 1 }); // https://github.com/lovell/sharp/issues/3176
115+
116+
if (metadata.size && metadata.size > (this._maxGifSizeMB * MB)) {
117+
console.log(`Gif processing skipped. The image size exceeds ${this._maxGifSizeMB} MB`);
118+
ctx.mask.disableAll();
119+
} else if (metadata.pages && metadata.pages > this._maxGifPages) {
120+
console.log(`Gif processing skipped. The image pages exceeds ${this._maxGifPages}`);
121+
ctx.mask.disableAll();
122+
}
97123
}
98124
if ('png' === metadata.format && metadata.size && metadata.size > (5 * MB)) {
99125
image.png({ adaptiveFiltering: true });
@@ -135,7 +161,7 @@ export class ImageProcessor implements IProcessor {
135161
act.beforeProcess.bind(act)(ctx, params, index);
136162
});
137163
const enabledActions = ctx.mask.filterEnabledActions();
138-
const nothing2do = (enabledActions.length === 1) && (this.name === enabledActions[0]);
164+
const nothing2do = (enabledActions.length === 0) || ((enabledActions.length === 1) && (this.name === enabledActions[0]));
139165

140166
if (nothing2do && (!ctx.features[Features.AutoWebp])) {
141167
const { buffer } = await ctx.bufferStore.get(ctx.uri);

0 commit comments

Comments
 (0)