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 sharp from "sharp";
|
||||||
import { getMinioClient } from "./minio";
|
import { getMinioClient } from "./minio";
|
||||||
import { lookup } from "mime-types";
|
import { lookup } from "mime-types";
|
||||||
|
import { PassThrough, pipeline } from "stream";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
const pump = promisify(pipeline);
|
||||||
|
|
||||||
export async function processImage(
|
export async function processImage(
|
||||||
bucket: string,
|
bucket: string,
|
||||||
@ -8,9 +12,9 @@ export async function processImage(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const minio = getMinioClient();
|
const minio = getMinioClient();
|
||||||
|
|
||||||
// Fetch metadata
|
|
||||||
const stat = await minio.statObject(bucket, key);
|
const stat = await minio.statObject(bucket, key);
|
||||||
const meta = stat as unknown as { metaData: Record<string, string> };
|
const meta = stat as unknown as { metaData: Record<string, string> };
|
||||||
|
|
||||||
const mime = meta.metaData["content-type"] || lookup(key) || "";
|
const mime = meta.metaData["content-type"] || lookup(key) || "";
|
||||||
|
|
||||||
if (!mime.startsWith("image/")) {
|
if (!mime.startsWith("image/")) {
|
||||||
@ -26,70 +30,102 @@ export async function processImage(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const originalStream = await minio.getObject(bucket, key);
|
|
||||||
|
|
||||||
const fileName = key.split("/").pop()!;
|
const fileName = key.split("/").pop()!;
|
||||||
const filePath = key.substring(0, key.lastIndexOf("/"));
|
const filePath = key.substring(0, key.lastIndexOf("/"));
|
||||||
|
const processedMeta = { "x-amz-meta-processed": "true" };
|
||||||
|
|
||||||
const processedMeta = {
|
const inputStream = await minio.getObject(bucket, key);
|
||||||
"x-amz-meta-processed": "true",
|
const sharpInstance = sharp();
|
||||||
};
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 🖼️ Thumbnail (resize to 200px width)
|
// Pipe MinIO stream into Sharp
|
||||||
await uploadFromStream(
|
inputStream.pipe(sharpInstance);
|
||||||
`${filePath}/thumbs/${fileName}`,
|
|
||||||
mime,
|
|
||||||
originalStream.pipe(sharp().resize(200))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Re-fetch original again for each variant (streams are one-time-use)
|
// Clone the sharp instance
|
||||||
const optimizedStream = await minio.getObject(bucket, key);
|
const thumbSharp = sharpInstance.clone().resize(200);
|
||||||
|
const jpegSharp = sharpInstance.clone().jpeg({ quality: 80 });
|
||||||
|
const webpSharp = sharpInstance.clone().webp({ quality: 80 });
|
||||||
|
|
||||||
// 📸 Optimized JPEG
|
// Prepare PassThroughs to collect processed outputs
|
||||||
await uploadFromStream(
|
const thumbStream = new PassThrough();
|
||||||
`${filePath}/optimized/${fileName}`,
|
const jpegStream = new PassThrough();
|
||||||
"image/jpeg",
|
const webpStream = new PassThrough();
|
||||||
optimizedStream.pipe(sharp().jpeg({ quality: 80 }))
|
|
||||||
);
|
|
||||||
|
|
||||||
const webpStream = await minio.getObject(bucket, key);
|
// Start Sharp pipelines
|
||||||
|
thumbSharp.pipe(thumbStream);
|
||||||
|
jpegSharp.pipe(jpegStream);
|
||||||
|
webpSharp.pipe(webpStream);
|
||||||
|
|
||||||
// 🌐 WebP version
|
// Upload all variants in parallel
|
||||||
const webpName = fileName.replace(/\.[^/.]+$/, ".webp");
|
await Promise.all([
|
||||||
await uploadFromStream(
|
uploadToMinio(
|
||||||
`${filePath}/webp/${webpName}`,
|
thumbStream,
|
||||||
"image/webp",
|
`${filePath}/thumbs/${fileName}`,
|
||||||
webpStream.pipe(sharp().webp({ quality: 80 }))
|
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 minio.putObject(bucket, key, originalBuffer, originalBuffer.length, {
|
||||||
await uploadFromStream(key, mime, finalOriginalStream);
|
"Content-Type": mime,
|
||||||
|
...processedMeta,
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`✅ Processed image: ${key}`);
|
console.log(`✅ Image processed: ${key}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`❌ Error processing image (${key}):`, err);
|
console.error(`❌ Error processing image (${key}):`, err);
|
||||||
return false;
|
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