fix memory leak
This commit is contained in:
parent
dff34fb103
commit
c0aa8c88ba
136
process-image.ts
136
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<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,
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user