Skip to content

Instantly share code, notes, and snippets.

@altrr2
Last active May 30, 2022 19:25
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save altrr2/30ca0635ff4dac03c00ad2c21d0288eb to your computer and use it in GitHub Desktop.
Save altrr2/30ca0635ff4dac03c00ad2c21d0288eb to your computer and use it in GitHub Desktop.
// Resize an image on the fly in Yandex Cloud from STORAGE_BUCKET to IMAGES_BUCKET
// https://aws.amazon.com/blogs/compute/resize-images-on-the-fly-with-amazon-s3-aws-lambda-and-amazon-api-gateway/
// on YC set up the following rules for the images bucket (as a website hosting):
// Condition
// Response code 404
// Key prefix resize
// Redirect
// Protocol HTTPS
// Domain name your_domain_name.tld
// Response code 307
// Replace key Prefix only
// New key prefix resize?key=resize
// APIGW setup:
// /resize/{proxy+}:
// get:
// x-yc-apigateway-integration:
// type: cloud_functions
// function_id: your_function_id
// tag: $latest
// service_account_id: your_function_sa_id
// parameters:
// - explode: true
// in: path
// name: proxy
// required: true
// schema:
// type: string
// style: simple
// responses:
// '301':
// description: 301 redirect
import {
S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectsCommand, ListObjectsCommand,
} from '@aws-sdk/client-s3';
import sharp from 'sharp';
import { Readable } from 'stream';
const storageBucket = process.env.STORAGE_BUCKET as string;
const imagesBucket = process.env.IMAGES_BUCKET as string;
const imagesUrl = process.env.IMAGES_URL as string;
let s3Client: S3Client;
const allowedDimensions = new Set<string>();
if (allowedDimensions.entries.length === 0 && process.env.RESIZE_ALLOWED_DIMENSIONS) {
const dimensions = (process.env.RESIZE_ALLOWED_DIMENSIONS).split(/\s*\/\s*/);
dimensions.forEach((dimension) => allowedDimensions.add(dimension));
}
export const handler: AWSLambda.Handler = async function(event, _context) {
const key = (event.queryStringParameters?.key ?? '') as string;
const match = key.match(/((\d+)x(\d+))\/(.*)/);
if (match == null || match.length !== 5) {
return {
statusCode: '403',
headers: {},
body: '',
};
}
const dimensions = match[1];
const width = parseInt(match[2], 10);
const height = parseInt(match[3], 10);
const originalKey = match[4];
const notFound = {
statusCode: '404',
headers: {},
body: 'Object not found. Key: ' + originalKey,
};
if (allowedDimensions.size > 0 && !allowedDimensions.has(dimensions)) {
return notFound;
}
const client = getS3Client();
if (dimensions === '0x0') {
if (originalKey.includes('.')) {
const deleteCommand = new DeleteObjectsCommand({
Bucket: imagesBucket,
Delete: { Objects: [] },
});
for (const d of allowedDimensions.values()) {
if (d !== '0x0') {
deleteCommand.input.Delete?.Objects?.push({ Key: key.replace('0x0', d) });
}
}
if ((deleteCommand.input.Delete?.Objects?.length ?? 0) !== 0) {
try {
await client.send(deleteCommand);
} catch (err) {
console.log(err);
}
}
} else {
const promises: Promise<unknown>[] = [];
for (const d of allowedDimensions.values()) {
if (d !== '0x0') {
promises.push(deleteFolder(client, key.replace('0x0', d)));
}
}
await Promise.all(promises);
}
return notFound;
}
try {
const getCommand = new GetObjectCommand({
Bucket: storageBucket,
Key: originalKey,
});
const data = await client.send(getCommand);
if (data.Body == null || !(data.Body instanceof Readable)) {
return notFound;
}
const pipeline = sharp();
const stream = pipeline.rotate().resize(width, height).toFormat('png');
data.Body.pipe(pipeline);
const putCommand = new PutObjectCommand({
Body: stream,
Bucket: imagesBucket,
ContentType: 'image/png',
Key: key,
});
await client.send(putCommand);
return {
statusCode: '301',
headers: { 'Location': `${imagesUrl}/${key}` },
body: '',
};
} catch (err) {
if (!(err instanceof Error && err.name === 'NoSuchKey')) {
console.log(err);
}
return notFound;
}
};
async function deleteFolder(client: S3Client, prefix: string) {
// 1000 objects limit?
const listCommand = new ListObjectsCommand({
Bucket: imagesBucket,
Prefix: prefix,
});
const deleteCommand = new DeleteObjectsCommand({
Bucket: imagesBucket,
Delete: { Objects: [] },
});
const listed = await client.send(listCommand);
if (listed.Contents != null && listed.Contents.length !== 0) {
listed.Contents.forEach(({ Key }) => {
deleteCommand.input.Delete?.Objects?.push({ Key });
});
}
if ((deleteCommand.input.Delete?.Objects?.length ?? 0) !== 0) {
try {
await client.send(deleteCommand);
} catch (err) {
console.log(err);
}
}
}
function getS3Client() {
if (s3Client == null) {
const config = {
endpoint: process.env.S3_ENDPOINT as string,
region: process.env.S3_REGION_NAME as string,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID as string,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY as string,
},
signatureVersion: 'v4',
};
s3Client = new S3Client(config);
}
return s3Client;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment