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, key: string ): Promise { const minio = getMinioClient(); 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/")) { console.log(`⏭️ Skipping non-image file: ${key}`); return false; } if ( meta.metaData["x-amz-meta-processed"] === "true" || meta.metaData["processed"] === "true" ) { console.log(`♻️ Already processed: ${key}`); return false; } const fileName = key.split("/").pop()!; const filePath = key.substring(0, key.lastIndexOf("/")); const processedMeta = { "x-amz-meta-processed": "true" }; const inputStream = await minio.getObject(bucket, key); const sharpInstance = sharp(); try { // Pipe MinIO stream into Sharp inputStream.pipe(sharpInstance); // Clone the sharp instance const thumbSharp = sharpInstance.clone().resize(200); const jpegSharp = sharpInstance.clone().jpeg({ quality: 80 }); const webpSharp = sharpInstance.clone().webp({ quality: 80 }); // Prepare PassThroughs to collect processed outputs const thumbStream = new PassThrough(); const jpegStream = new PassThrough(); const webpStream = new PassThrough(); // Start Sharp pipelines thumbSharp.pipe(thumbStream); jpegSharp.pipe(jpegStream); webpSharp.pipe(webpStream); // 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 ), ]); // 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); await minio.putObject(bucket, key, originalBuffer, originalBuffer.length, { "Content-Type": mime, ...processedMeta, }); 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, }); }