fix memory leak

This commit is contained in:
Nimer Farahty 2025-04-26 20:08:17 +03:00
parent dff34fb103
commit c0aa8c88ba

View File

@ -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<boolean> {
const minio = getMinioClient();
// Fetch metadata
const stat = await minio.statObject(bucket, key);
const meta = stat as unknown as { metaData: Record<string, string> };
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<string, string>
) {
const minio = getMinioClient();
const chunks: Buffer[] = [];
stream.on("data", (chunk) => chunks.push(chunk));
await new Promise<void>((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,
});
}