image processing lambda
This commit is contained in:
161
lambdas/imageProcessor/index.js
Normal file
161
lambdas/imageProcessor/index.js
Normal file
@@ -0,0 +1,161 @@
|
||||
const {
|
||||
S3Client,
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
} = require("@aws-sdk/client-s3");
|
||||
const { extractMetadata, stripMetadata, getMimeType } = require("./imageProcessor");
|
||||
const { saveImageMetadata, updateProcessingStatus } = require("./queries");
|
||||
const { logger } = require("../shared");
|
||||
|
||||
const s3Client = new S3Client({ region: process.env.AWS_REGION });
|
||||
|
||||
/**
|
||||
* Lambda handler for S3 image processing.
|
||||
* Triggered by S3 ObjectCreated events on staging/ prefix.
|
||||
*
|
||||
* @param {Object} event - S3 event
|
||||
* @returns {Object} Processing results
|
||||
*/
|
||||
exports.handler = async (event) => {
|
||||
logger.info("Lambda invoked", { recordCount: event.Records?.length });
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const record of event.Records) {
|
||||
const bucket = record.s3.bucket.name;
|
||||
const stagingKey = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
|
||||
|
||||
logger.info("Processing image", { bucket, stagingKey });
|
||||
|
||||
try {
|
||||
// Only process files in staging/ folder
|
||||
if (!stagingKey.startsWith("staging/")) {
|
||||
logger.info("Skipping non-staging key", { stagingKey });
|
||||
results.push({ key: stagingKey, status: "skipped", reason: "not in staging" });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate final key: staging/items/uuid.jpg -> items/uuid.jpg
|
||||
const finalKey = stagingKey.replace(/^staging\//, "");
|
||||
|
||||
// Check if this is an image file
|
||||
if (!isImageFile(stagingKey)) {
|
||||
logger.info("Skipping non-image file", { stagingKey });
|
||||
results.push({ key: stagingKey, status: "skipped", reason: "not an image" });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process the image
|
||||
await processImage(bucket, stagingKey, finalKey);
|
||||
|
||||
results.push({ key: finalKey, status: "success" });
|
||||
logger.info("Successfully processed image", { finalKey });
|
||||
} catch (error) {
|
||||
logger.error("Error processing image", { stagingKey, error: error.message, stack: error.stack });
|
||||
results.push({ key: stagingKey, status: "error", error: error.message });
|
||||
|
||||
// Try to update status to failed if we have a finalKey
|
||||
try {
|
||||
const finalKey = stagingKey.replace(/^staging\//, "");
|
||||
await updateProcessingStatus(finalKey, "failed", error.message);
|
||||
} catch (dbError) {
|
||||
logger.error("Failed to update error status in DB", { error: dbError.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify({ processed: results.length, results }),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Process a single image: extract metadata, strip, and move to final location.
|
||||
*
|
||||
* @param {string} bucket - S3 bucket name
|
||||
* @param {string} stagingKey - Staging key (e.g., staging/items/uuid.jpg)
|
||||
* @param {string} finalKey - Final key (e.g., items/uuid.jpg)
|
||||
*/
|
||||
async function processImage(bucket, stagingKey, finalKey) {
|
||||
// 1. Download image from staging location
|
||||
logger.debug("Downloading from staging", { stagingKey });
|
||||
const getCommand = new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: stagingKey,
|
||||
});
|
||||
const response = await s3Client.send(getCommand);
|
||||
const buffer = Buffer.from(await response.Body.transformToByteArray());
|
||||
|
||||
// 2. Extract metadata BEFORE stripping
|
||||
logger.debug("Extracting metadata");
|
||||
const metadata = await extractMetadata(buffer);
|
||||
logger.info("Extracted metadata", { finalKey, metadata });
|
||||
|
||||
// 3. Save metadata to database
|
||||
logger.debug("Saving metadata to DB", { finalKey });
|
||||
await saveImageMetadata(finalKey, metadata);
|
||||
|
||||
// 4. Strip metadata from image
|
||||
logger.debug("Stripping metadata");
|
||||
const strippedBuffer = await stripMetadata(buffer);
|
||||
|
||||
// 5. Determine content type from original
|
||||
const format = stagingKey.split(".").pop().toLowerCase();
|
||||
const contentType = getMimeType(format);
|
||||
|
||||
// 6. Upload clean image to FINAL location
|
||||
logger.debug("Uploading to final location", { finalKey });
|
||||
const putCommand = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: finalKey,
|
||||
Body: strippedBuffer,
|
||||
ContentType: contentType,
|
||||
CacheControl: getCacheControl(finalKey),
|
||||
Metadata: {
|
||||
"x-processed": "true",
|
||||
"x-processed-at": new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
await s3Client.send(putCommand);
|
||||
|
||||
// 7. Delete staging file
|
||||
logger.debug("Deleting staging file", { stagingKey });
|
||||
const deleteCommand = new DeleteObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: stagingKey,
|
||||
});
|
||||
await s3Client.send(deleteCommand);
|
||||
|
||||
// 8. Update processing status to completed
|
||||
logger.debug("Updating processing status to completed");
|
||||
await updateProcessingStatus(finalKey, "completed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is an image based on extension.
|
||||
*
|
||||
* @param {string} key - S3 key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isImageFile(key) {
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
||||
const ext = key.toLowerCase().slice(key.lastIndexOf("."));
|
||||
return imageExtensions.includes(ext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Cache-Control header based on folder.
|
||||
*
|
||||
* @param {string} key - S3 key
|
||||
* @returns {string}
|
||||
*/
|
||||
function getCacheControl(key) {
|
||||
// Private folders get shorter cache
|
||||
if (key.startsWith("messages/") || key.startsWith("condition-checks/")) {
|
||||
return "private, max-age=3600";
|
||||
}
|
||||
// Public folders get longer cache
|
||||
return "public, max-age=86400";
|
||||
}
|
||||
Reference in New Issue
Block a user