minio-worker/process-image.ts
2025-04-26 20:08:17 +03:00

132 lines
3.4 KiB
TypeScript

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<boolean> {
const minio = getMinioClient();
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/")) {
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<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,
});
}