From e8380df96bbf981c18214c14cad9e170b562ddf6 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Wed, 6 May 2026 10:10:38 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20Lambda=20=EB=AA=A8=EB=93=88=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상세내용: nodejs22.x 런타임에서 index.js가 CommonJS로 로드되도록 import/export 문법을 require/exports로 변경 - 상세내용: resizing과 thumbnail Lambda 모두 S3 이벤트 record 검증과 다중 record 처리를 추가 - 상세내용: thumbnail 결과 확장자를 실제 JPEG 변환 결과와 맞춰 .jpg로 저장 --- .../src/img_resizing/index.js | 117 ++++++----- .../shared_resources/src/thumbnail/index.js | 194 +++++++++--------- 2 files changed, 153 insertions(+), 158 deletions(-) diff --git a/modules/shared_resources/src/img_resizing/index.js b/modules/shared_resources/src/img_resizing/index.js index 6baaf61..d7e644e 100644 --- a/modules/shared_resources/src/img_resizing/index.js +++ b/modules/shared_resources/src/img_resizing/index.js @@ -1,81 +1,80 @@ -// dependencies -import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; -import { Readable } from "stream"; -import sharp from "sharp"; -import util from "util"; +const { S3Client, GetObjectCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); +const sharp = require("sharp"); +const util = require("util"); -// create S3 client const s3 = new S3Client({ region: "ap-northeast-2" }); +const SOURCE_PREFIX = "original/"; +const DESTINATION_PREFIX = "resize/"; +const SUPPORTED_IMAGE_TYPES = new Set(["jpg", "jpeg", "png"]); -// define the handler function -export const handler = async (event, context) => { - // Read options from the event parameter and get the source bucket +exports.handler = async (event) => { console.log("Reading options from event:\n", util.inspect(event, { depth: 5 })); - const srcBucket = event.Records[0].s3.bucket.name; - // Object key may have spaces or unicode non-ASCII characters - const srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); - const dstBucket = srcBucket; // Destination bucket is the same as source bucket - const dstKey = srcKey.replace("original", "resize").replace(/\.\w+$/, ".webp"); // Change directory and file extension + const records = event?.Records ?? []; + if (records.length === 0) { + console.log("No S3 records found. Skipping."); + return; + } + + for (const record of records) { + await resizeImage(record); + } +}; + +async function resizeImage(record) { + const srcBucket = record?.s3?.bucket?.name; + const objectKey = record?.s3?.object?.key; + + if (!srcBucket || !objectKey) { + console.log("Invalid S3 record. Skipping."); + return; + } + + const srcKey = decodeURIComponent(objectKey.replace(/\+/g, " ")); + if (!srcKey.startsWith(SOURCE_PREFIX)) { + console.log(`Not an original image. Skipping: ${srcKey}`); + return; + } - // Infer the image type from the file suffix const typeMatch = srcKey.match(/\.([^.]*)$/); if (!typeMatch) { - console.log("Could not determine the image type."); + console.log(`Could not determine the image type: ${srcKey}`); return; } - // Supported image types for Sharp const imageType = typeMatch[1].toLowerCase(); - if (imageType != "jpg" && imageType != "png") { + if (!SUPPORTED_IMAGE_TYPES.has(imageType)) { console.log(`Unsupported image type: ${imageType}`); return; } - try { - const params = { - Bucket: srcBucket, - Key: srcKey, - }; - var response = await s3.send(new GetObjectCommand(params)); - var stream = response.Body; + const dstKey = srcKey.replace(SOURCE_PREFIX, DESTINATION_PREFIX).replace(/\.[^.]+$/, ".webp"); - if (stream instanceof Readable) { - var content_buffer = Buffer.concat(await stream.toArray()); - } else { - throw new Error("Unknown object stream type"); - } - } catch (error) { - console.log(error); - return; - } + const response = await s3.send(new GetObjectCommand({ + Bucket: srcBucket, + Key: srcKey, + })); - const width = 600; // set thumbnail width + const contentBuffer = await streamToBuffer(response.Body); + const outputBuffer = await sharp(contentBuffer) + .resize(600) + .webp() + .toBuffer(); - try { - var output_buffer = await sharp(content_buffer) - .resize(width) - .webp() // Convert to webp - .toBuffer(); - } catch (error) { - console.log(error); - return; - } + await s3.send(new PutObjectCommand({ + Bucket: srcBucket, + Key: dstKey, + Body: outputBuffer, + ContentType: "image/webp", + })); - // Upload the resized .webp image to the destination bucket - try { - const destparams = { - Bucket: dstBucket, - Key: dstKey, - Body: output_buffer, - ContentType: "image/webp", // Set the correct Content-Type - }; + console.log(`Successfully resized ${srcBucket}/${srcKey} and uploaded to ${srcBucket}/${dstKey}`); +} - await s3.send(new PutObjectCommand(destparams)); - } catch (error) { - console.log(error); - return; +async function streamToBuffer(stream) { + const chunks = []; + for await (const chunk of stream) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } - - console.log("Successfully resized " + srcBucket + "/" + srcKey + " and uploaded to " + dstBucket + "/" + dstKey); -}; + return Buffer.concat(chunks); +} diff --git a/modules/shared_resources/src/thumbnail/index.js b/modules/shared_resources/src/thumbnail/index.js index fed4a9d..bc07806 100644 --- a/modules/shared_resources/src/thumbnail/index.js +++ b/modules/shared_resources/src/thumbnail/index.js @@ -1,108 +1,104 @@ -import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; -import sharp from 'sharp'; - -const s3Client = new S3Client({ region: 'ap-northeast-2' }); - -export const handler = async (event) => { - console.log('Event received:', JSON.stringify(event, null, 2)); - - try { - // S3 이벤트에서 버킷과 객체 정보 추출 - const record = event.Records[0]; - const bucket = record.s3.bucket.name; - const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' ')); - - console.log(`Processing file: ${key} from bucket: ${bucket}`); - - // chat/files/ 폴더의 이미지 파일만 처리 - if (!key.startsWith('chat/files/')) { - console.log('Not a chat file, skipping'); - return { statusCode: 200, body: 'Not a chat file' }; - } - - // 이미지 파일 확장자 확인 - const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp']; - if (!imageExtensions.some(ext => key.toLowerCase().endsWith(ext))) { - console.log('Not an image file, skipping'); - return { statusCode: 200, body: 'Not an image file' }; - } - - // 이미 썸네일 파일인 경우 처리하지 않음 (무한 루프 방지) - if (key.includes('_thumb')) { - console.log('Already a thumbnail, skipping'); - return { statusCode: 200, body: 'Already a thumbnail' }; - } - - // 원본 이미지 다운로드 (AWS SDK v3 방식) - console.log('Downloading original image...'); - const getCommand = new GetObjectCommand({ Bucket: bucket, Key: key }); - const response = await s3Client.send(getCommand); - - // Body를 Buffer로 변환 - const imageBuffer = await streamToBuffer(response.Body); - - // Sharp를 사용해 썸네일 생성 - console.log('Creating thumbnail...'); - const thumbnailBuffer = await sharp(imageBuffer) - .resize(200, 200, { - fit: 'inside', // 비율 유지하면서 200x200 안에 맞춤 - withoutEnlargement: true // 원본보다 크게 만들지 않음 - }) - .jpeg({ quality: 85 }) // JPEG 품질 85% - .toBuffer(); - - // 썸네일 파일명 생성 - const fileName = key.split('/').pop(); // 파일명만 추출 - const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')); - const extension = fileName.substring(fileName.lastIndexOf('.')); - const thumbnailKey = `chat/thumbnails/${nameWithoutExt}_thumb${extension}`; - - console.log(`Uploading thumbnail to: ${thumbnailKey}`); - - // 썸네일을 S3에 업로드 (AWS SDK v3 방식) - const putCommand = new PutObjectCommand({ - Bucket: bucket, - Key: thumbnailKey, - Body: thumbnailBuffer, - ContentType: 'image/jpeg', - Metadata: { - 'original-key': key, - 'generated-by': 'thumbnail-lambda' - } - }); - - await s3Client.send(putCommand); - - console.log(`Thumbnail created successfully: ${thumbnailKey}`); - - return { - statusCode: 200, - body: JSON.stringify({ - message: 'Thumbnail created successfully', - original: key, - thumbnail: thumbnailKey, - thumbnailSize: thumbnailBuffer.length - }) - }; - - } catch (error) { - console.error('Error creating thumbnail:', error); - - return { - statusCode: 500, - body: JSON.stringify({ - message: 'Error creating thumbnail', - error: error.message - }) - }; +const { S3Client, GetObjectCommand, PutObjectCommand } = require("@aws-sdk/client-s3"); +const sharp = require("sharp"); + +const s3 = new S3Client({ region: "ap-northeast-2" }); +const SOURCE_PREFIX = "chat/files/"; +const THUMBNAIL_PREFIX = "chat/thumbnails/"; +const SUPPORTED_IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp"]); + +exports.handler = async (event) => { + console.log("Event received:", JSON.stringify(event, null, 2)); + + const records = event?.Records ?? []; + if (records.length === 0) { + console.log("No S3 records found. Skipping."); + return { statusCode: 200, body: "No S3 records found" }; + } + + const results = []; + for (const record of records) { + results.push(await createThumbnail(record)); } + + return { + statusCode: 200, + body: JSON.stringify({ results }), + }; }; -// Stream을 Buffer로 변환하는 헬퍼 함수 +async function createThumbnail(record) { + const bucket = record?.s3?.bucket?.name; + const objectKey = record?.s3?.object?.key; + + if (!bucket || !objectKey) { + console.log("Invalid S3 record. Skipping."); + return { skipped: true, reason: "Invalid S3 record" }; + } + + const key = decodeURIComponent(objectKey.replace(/\+/g, " ")); + console.log(`Processing file: ${key} from bucket: ${bucket}`); + + if (!key.startsWith(SOURCE_PREFIX)) { + console.log("Not a chat file, skipping"); + return { skipped: true, reason: "Not a chat file", key }; + } + + const extension = getExtension(key); + if (!SUPPORTED_IMAGE_EXTENSIONS.has(extension)) { + console.log("Not an image file, skipping"); + return { skipped: true, reason: "Not an image file", key }; + } + + if (key.includes("_thumb")) { + console.log("Already a thumbnail, skipping"); + return { skipped: true, reason: "Already a thumbnail", key }; + } + + const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); + const imageBuffer = await streamToBuffer(response.Body); + + const thumbnailBuffer = await sharp(imageBuffer) + .resize(200, 200, { + fit: "inside", + withoutEnlargement: true, + }) + .jpeg({ quality: 85 }) + .toBuffer(); + + const fileName = key.split("/").pop(); + const nameWithoutExt = fileName.slice(0, -extension.length); + const thumbnailKey = `${THUMBNAIL_PREFIX}${nameWithoutExt}_thumb.jpg`; + + await s3.send(new PutObjectCommand({ + Bucket: bucket, + Key: thumbnailKey, + Body: thumbnailBuffer, + ContentType: "image/jpeg", + Metadata: { + "original-key": key, + "generated-by": "thumbnail-lambda", + }, + })); + + console.log(`Thumbnail created successfully: ${thumbnailKey}`); + + return { + skipped: false, + original: key, + thumbnail: thumbnailKey, + thumbnailSize: thumbnailBuffer.length, + }; +} + +function getExtension(key) { + const dotIndex = key.lastIndexOf("."); + return dotIndex === -1 ? "" : key.slice(dotIndex).toLowerCase(); +} + async function streamToBuffer(stream) { const chunks = []; for await (const chunk of stream) { - chunks.push(chunk); + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } return Buffer.concat(chunks); }