aws-cdk-image-resize
Version:
AWS CDK construct to easily setup the required arquitecture to serve performant responsive images.
118 lines (99 loc) • 3.69 kB
JavaScript
/* eslint-disable @typescript-eslint/no-var-requires */
const sharp = require('sharp');
const AWS = require('aws-sdk');
const s3 = new AWS.S3({ signatureVersion: 'v4' });
/**
* @param {{uri: String}} request
* @returns {{key: String, prefix: String, extension: String, width?: Number, height?: Number}}
*/
exports.extractDataFromUri = request => {
const uri = request.uri;
// AWS key is the URI without the initial '/'
const key = uri.substring(1);
// Try to match dimensions first
// e.g.: /path/to/file-100wx100h.webp
const dimensionMatch = uri.match(/\/(.*)-([0-9]+)wx([0-9]+)h\.([^.]*)$/);
if (dimensionMatch)
return {
key,
prefix: dimensionMatch[1],
width: parseInt(dimensionMatch[2]),
height: parseInt(dimensionMatch[3]),
extension: dimensionMatch[4],
};
// If no dimensions included, we just care about the prefix and the extension
const simpleMatch = uri.match(/\/(.*)\.([^.]*)$/);
return { key, prefix: simpleMatch[1], extension: simpleMatch[2] };
};
exports.handler = async (event, _context, callback) => {
const response = event.Records[0].cf.response;
const request = event.Records[0].cf.request;
// Extracting bucket name. domainName looks like this: bucket-name.s3.region.amazonaws.com"
const [, Bucket] = request.origin.s3.domainName.match(/(.*).s3./);
if (Number(response.status) !== 404) {
if (Number(response.status) !== 200) response.status = 400;
callback(null, response);
return;
}
// Image not found in bucket
let params;
try {
params = this.extractDataFromUri(request);
} catch (e) {
callback(null, response);
return;
}
const { Contents } = await s3
.listObjects({
Bucket,
// List all keys starting with path/to/file.
Prefix: params.prefix + '.',
})
.promise();
if (!Contents.length) {
callback(null, response);
return;
}
const baseImageKey = (() => {
/**
* Try to find an existent image for the requested extension.
* If there isn't one, the use as base image the first from the Contents array
*/
const found = Contents.find(({ Key }) => Key.split(`${params.prefix}.`)[1] === params.extension);
if (found) return found.Key;
return Contents[0].Key;
})();
// Use the found key to get the image from the s3 bucket
const { Body, ContentType } = await s3.getObject({ Key: baseImageKey, Bucket }).promise();
const sharpPromise = sharp(Body);
// If dimensions passed, resize base image
if (params.width || params.height) {
// Allow to pass only one of width or height
sharpPromise.resize({
fit: 'inside',
height: params.height || undefined,
width: params.width || undefined,
withoutEnlargement: true,
});
}
// If the requested extension is different than the base image extension, then
// format it to the new extension
if (ContentType !== `image/${params.extension}`) sharpPromise.toFormat(params.extension);
const buffer = await sharpPromise.toBuffer();
// Save the new image to s3 bucket. Don't await for this to finish.
// Even if the upload fails we return the converted image
s3.putObject({
Body: buffer,
Bucket,
ContentType: 'image/' + params.extension,
CacheControl: 'max-age=31536000',
Key: params.key,
StorageClass: 'STANDARD',
}).promise();
response.status = 200;
response.body = buffer.toString('base64');
response.bodyEncoding = 'base64';
response.headers['content-type'] = [{ key: 'Content-Type', value: 'image/' + params.extension }];
response.headers['cache-control'] = [{ key: 'Cache-Control', value: 'max-age=31536000' }];
callback(null, response);
};