132 lines
3.4 KiB
TypeScript
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,
|
|
});
|
|
}
|