diff --git a/process-image.ts b/process-image.ts index 8d3b505..9aa10d7 100644 --- a/process-image.ts +++ b/process-image.ts @@ -1,6 +1,10 @@ import sharp from "sharp"; import { getMinioClient } from "./minio"; import { lookup } from "mime-types"; +import { PassThrough, pipeline } from "stream"; +import { promisify } from "util"; + +const pump = promisify(pipeline); export async function processImage( bucket: string, @@ -8,9 +12,9 @@ export async function processImage( ): Promise { const minio = getMinioClient(); - // Fetch metadata const stat = await minio.statObject(bucket, key); const meta = stat as unknown as { metaData: Record }; + const mime = meta.metaData["content-type"] || lookup(key) || ""; if (!mime.startsWith("image/")) { @@ -26,70 +30,102 @@ export async function processImage( return false; } - const originalStream = await minio.getObject(bucket, key); - const fileName = key.split("/").pop()!; const filePath = key.substring(0, key.lastIndexOf("/")); + const processedMeta = { "x-amz-meta-processed": "true" }; - const processedMeta = { - "x-amz-meta-processed": "true", - }; - - // Helper to upload from a stream - async function uploadFromStream( - targetPath: string, - mimeType: string, - transformStream: NodeJS.ReadableStream - ) { - const chunks: Buffer[] = []; - for await (const chunk of transformStream) { - chunks.push(chunk as Buffer); - } - const finalBuffer = Buffer.concat(chunks); - - await minio.putObject(bucket, targetPath, finalBuffer, finalBuffer.length, { - "Content-Type": mimeType, - ...processedMeta, - }); - } + const inputStream = await minio.getObject(bucket, key); + const sharpInstance = sharp(); try { - // 🖼️ Thumbnail (resize to 200px width) - await uploadFromStream( - `${filePath}/thumbs/${fileName}`, - mime, - originalStream.pipe(sharp().resize(200)) - ); + // Pipe MinIO stream into Sharp + inputStream.pipe(sharpInstance); - // Re-fetch original again for each variant (streams are one-time-use) - const optimizedStream = await minio.getObject(bucket, key); + // Clone the sharp instance + const thumbSharp = sharpInstance.clone().resize(200); + const jpegSharp = sharpInstance.clone().jpeg({ quality: 80 }); + const webpSharp = sharpInstance.clone().webp({ quality: 80 }); - // 📸 Optimized JPEG - await uploadFromStream( - `${filePath}/optimized/${fileName}`, - "image/jpeg", - optimizedStream.pipe(sharp().jpeg({ quality: 80 })) - ); + // Prepare PassThroughs to collect processed outputs + const thumbStream = new PassThrough(); + const jpegStream = new PassThrough(); + const webpStream = new PassThrough(); - const webpStream = await minio.getObject(bucket, key); + // Start Sharp pipelines + thumbSharp.pipe(thumbStream); + jpegSharp.pipe(jpegStream); + webpSharp.pipe(webpStream); - // 🌐 WebP version - const webpName = fileName.replace(/\.[^/.]+$/, ".webp"); - await uploadFromStream( - `${filePath}/webp/${webpName}`, - "image/webp", - webpStream.pipe(sharp().webp({ quality: 80 })) - ); + // Upload all variants in parallel + await Promise.all([ + uploadToMinio( + thumbStream, + `${filePath}/thumbs/${fileName}`, + mime, + bucket, + processedMeta + ), + uploadToMinio( + jpegStream, + `${filePath}/optimized/${fileName}`, + "image/jpeg", + bucket, + processedMeta + ), + uploadToMinio( + webpStream, + `${filePath}/webp/${fileName.replace(/\.[^/.]+$/, ".webp")}`, + "image/webp", + bucket, + processedMeta + ), + ]); - const finalOriginalStream = await minio.getObject(bucket, key); + // Finally, reupload the original with processed metadata + const origBufferChunks: Buffer[] = []; + for await (const chunk of sharpInstance.clone()) { + origBufferChunks.push(chunk as Buffer); + } + const originalBuffer = Buffer.concat(origBufferChunks); - // 🔁 Re-upload the original with updated metadata to mark it processed - await uploadFromStream(key, mime, finalOriginalStream); + await minio.putObject(bucket, key, originalBuffer, originalBuffer.length, { + "Content-Type": mime, + ...processedMeta, + }); - console.log(`✅ Processed image: ${key}`); + console.log(`✅ Image processed: ${key}`); return true; } catch (err) { console.error(`❌ Error processing image (${key}):`, err); return false; + } finally { + inputStream.destroy(); + sharpInstance.destroy(); } } + +// Helper function to upload a stream +async function uploadToMinio( + stream: NodeJS.ReadableStream, + path: string, + mimeType: string, + bucket: string, + metadata: Record +) { + const minio = getMinioClient(); + + const chunks: Buffer[] = []; + stream.on("data", (chunk) => chunks.push(chunk)); + + await new Promise((resolve, reject) => { + stream.on("end", resolve); + stream.on("error", reject); + }); + + const buffer = Buffer.concat(chunks); + + await minio.putObject(bucket, path, buffer, buffer.length, { + "Content-Type": mimeType, + ...metadata, + }); +}