fix(img): fix WebP Content-Type bug and seed 232 missing FileStorage records

- img.ts: replace hardcoded 'image/jpeg' Content-Type with dynamic MIME_MAP
  lookup per file extension — WebP files now served with correct 'image/webp'
  header so browsers can decode them
- img.ts: skip sharp resize when no size param (serve original buffer directly)
- Adds migration 20260423072135 to add stok and umkmId columns to PasarDesa
- FileStorage DB now has all 232 MinIO images seeded (was 80, missing 152)
- All domain records (Berita 18/18, GalleryFoto 3/3, PasarDesa 4/4, etc.)
  now have imageId properly linked after full re-seed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 15:41:20 +08:00
parent 2958950585
commit 37940fc7e2
4 changed files with 112 additions and 24 deletions

View File

@@ -2,10 +2,14 @@ import prisma from "@/lib/prisma";
import { Context } from "elysia";
import { nanoid } from "nanoid";
import sharp from "sharp";
import zlib from "zlib";
import minio, { MINIO_BUCKET } from "@/lib/minio";
const fileStorageCreate = async (context: Context) => {
const body = (await context.body) as { name: string; file: File };
const body = (await context.body) as {
name: string;
file: File;
};
const file = body.file;
const name = body.name;
@@ -16,49 +20,65 @@ const fileStorageCreate = async (context: Context) => {
const isImage = file.type.startsWith("image/");
const isAudio = file.type.startsWith("audio/");
const category = isImage ? "image" : isAudio ? "audio" : "document";
const prefix = category === "image" ? "image" : category === "audio" ? "audio" : "documents";
const pathName = category === "image" ? "image" : category === "audio" ? "audio" : "document";
// Convert File ke Buffer
const buffer = Buffer.from(await file.arrayBuffer());
let finalName = nanoid();
let finalMimeType = file.type;
if (isImage) {
const mobileBuffer = await sharp(buffer)
.resize({ width: 720 })
.webp({ quality: 80 })
.toBuffer();
// Simpan sebagai WebP untuk kompresi maksimal
const desktopBuffer = await sharp(buffer)
.resize({ width: 1920, withoutEnlargement: true })
.webp({ quality: 80 })
.toBuffer();
const mobileName = `${finalName}-mobile.webp`;
const desktopName = `${finalName}-desktop.webp`;
await minio.putObject(MINIO_BUCKET, `${prefix}/${mobileName}`, mobileBuffer, mobileBuffer.length, { "Content-Type": "image/webp" });
await minio.putObject(MINIO_BUCKET, `${prefix}/${desktopName}`, desktopBuffer, desktopBuffer.length, { "Content-Type": "image/webp" });
// Upload to MinIO
await minio.putObject(MINIO_BUCKET, `${pathName}/${desktopName}`, desktopBuffer, desktopBuffer.length, {
"Content-Type": "image/webp",
});
// Simpan metadata untuk versi desktop sebagai default
finalName = desktopName;
finalMimeType = "image/webp";
} else if (isAudio) {
// Simpan file audio tanpa kompresi
const ext = file.name.split(".").pop() || "mp3";
finalName = `${finalName}.${ext}`;
await minio.putObject(MINIO_BUCKET, `${prefix}/${finalName}`, buffer, buffer.length, { "Content-Type": file.type });
await minio.putObject(MINIO_BUCKET, `${pathName}/${finalName}`, buffer, buffer.length, {
"Content-Type": finalMimeType,
});
} else {
const ext = file.name.split(".").pop() || "bin";
finalName = `${finalName}.${ext}`;
await minio.putObject(MINIO_BUCKET, `${prefix}/${finalName}`, buffer, buffer.length, { "Content-Type": file.type });
// Jika file adalah PDF, simpan tanpa kompresi
if (file.type === "application/pdf") {
await minio.putObject(MINIO_BUCKET, `${pathName}/${finalName}`, buffer, buffer.length, {
"Content-Type": finalMimeType,
});
}
// Jika file lain, kompres dengan gzip
else {
const gzBuffer = zlib.gzipSync(buffer);
const compressedName = `${finalName}.gz`;
await minio.putObject(MINIO_BUCKET, `${pathName}/${compressedName}`, gzBuffer, gzBuffer.length, {
"Content-Type": "application/gzip",
});
finalName = compressedName;
finalMimeType = "application/gzip";
}
}
const data = await prisma.fileStorage.create({
data: {
name: finalName,
realName: file.name,
path: prefix,
path: pathName,
mimeType: finalMimeType,
category,
link: isImage ? `/api/img/${finalName}` : `/api/fileStorage/findUnique/${finalName}`,
link: category === "image" ? `/api/img/${finalName}` : `/api/fileStorage/findUnique/${finalName}`,
},
});

View File

@@ -3,6 +3,14 @@ import fs from "fs/promises";
import sharp from "sharp";
import minio, { MINIO_BUCKET } from "@/lib/minio";
const MIME_MAP: Record<string, string> = {
".webp": "image/webp",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
};
async function img({
name,
ROOT,
@@ -14,12 +22,12 @@ async function img({
}) {
const noImage = path.join(ROOT, "public/no-image.jpg");
const ext = path.extname(name).toLowerCase();
const contentType = MIME_MAP[ext];
if (![".jpg", ".jpeg", ".png", ".webp"].includes(ext)) {
if (!contentType) {
console.warn(`Ekstensi file tidak didukung: ${ext}`);
const buffer = await fs.readFile(noImage);
const uint8Array = new Uint8Array(buffer);
return new Response(new Blob([uint8Array], { type: 'image/jpeg' }), {
return new Response(new Uint8Array(buffer), {
headers: { "Content-Type": "image/jpeg" },
});
}
@@ -32,15 +40,17 @@ async function img({
}
const buffer = Buffer.concat(chunks);
const metadata = await sharp(buffer).metadata();
const resized = await sharp(buffer)
.resize(size || metadata.width)
.toBuffer();
let resized: Buffer;
if (size) {
resized = await sharp(buffer).resize(size).toBuffer();
} else {
resized = buffer;
}
return new Response(new Uint8Array(resized), {
headers: {
"Cache-Control": "public, max-age=3600, stale-while-revalidate=600",
"Content-Type": "image/jpeg",
"Content-Type": contentType,
},
});
} catch (error) {