diff --git a/bun.lockb b/bun.lockb index 624c5ecf..2d40424d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d6976f97..2c8c3a0e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "framer-motion": "^12.4.1", "get-port": "^7.1.0", "lodash": "^4.17.21", + "minio": "^8.0.7", "motion": "^12.4.1", "nanoid": "^5.1.0", "next": "15.1.6", diff --git a/src/app/api/[[...slugs]]/_lib/img-del.ts b/src/app/api/[[...slugs]]/_lib/img-del.ts index 22b0c7f2..641e8653 100644 --- a/src/app/api/[[...slugs]]/_lib/img-del.ts +++ b/src/app/api/[[...slugs]]/_lib/img-del.ts @@ -1,15 +1,8 @@ -import fs from "fs/promises"; -import path from "path"; +import minio, { MINIO_BUCKET } from "@/lib/minio"; -async function imgDel({ - name, - UPLOAD_DIR_IMAGE, -}: { - name: string; - UPLOAD_DIR_IMAGE: string; -}) { +async function imgDel({ name }: { name: string }) { try { - await fs.unlink(path.join(UPLOAD_DIR_IMAGE, name)); + await minio.removeObject(MINIO_BUCKET, `image/${name}`); return "ok"; } catch (error) { console.log(error); diff --git a/src/app/api/[[...slugs]]/_lib/img.ts b/src/app/api/[[...slugs]]/_lib/img.ts index 4752afbf..e41bb4a7 100644 --- a/src/app/api/[[...slugs]]/_lib/img.ts +++ b/src/app/api/[[...slugs]]/_lib/img.ts @@ -1,27 +1,21 @@ -import fs from "fs/promises"; import path from "path"; +import fs from "fs/promises"; import sharp from "sharp"; +import minio, { MINIO_BUCKET } from "@/lib/minio"; async function img({ name, - UPLOAD_DIR_IMAGE, ROOT, size, }: { name: string; - UPLOAD_DIR_IMAGE: string; ROOT: string; - size?: number; // Ukuran opsional (tidak ada default) + size?: number; }) { - const completeName = path.basename(name); // Nama file lengkap - const ext = path.extname(name).toLowerCase(); // Ekstensi file dalam huruf kecil - // const fileNameWithoutExt = path.basename(name, ext); // Nama file tanpa ekstensi - - // Default image jika terjadi kesalahan const noImage = path.join(ROOT, "public/no-image.jpg"); + const ext = path.extname(name).toLowerCase(); - // Validasi ekstensi file - if (![".jpg", ".jpeg", ".png"].includes(ext)) { + if (![".jpg", ".jpeg", ".png", ".webp"].includes(ext)) { console.warn(`Ekstensi file tidak didukung: ${ext}`); return new Response(await fs.readFile(noImage), { headers: { "Content-Type": "image/jpeg" }, @@ -29,29 +23,26 @@ async function img({ } try { - // Path ke file asli - const filePath = path.join(UPLOAD_DIR_IMAGE, completeName); + const stream = await minio.getObject(MINIO_BUCKET, `image/${name}`); + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(Buffer.from(chunk)); + } + const buffer = Buffer.concat(chunks); - // Periksa apakah file ada - await fs.stat(filePath); - - // Metadata gambar asli - const metadata = await sharp(filePath).metadata(); - - // Proses resize menggunakan sharp - const resizedImageBuffer = await sharp(filePath) - .resize(size || metadata.width) // Gunakan size jika diberikan, jika tidak gunakan width asli + const metadata = await sharp(buffer).metadata(); + const resized = await sharp(buffer) + .resize(size || metadata.width) .toBuffer(); - return new Response(resizedImageBuffer, { + return new Response(resized, { headers: { "Cache-Control": "public, max-age=3600, stale-while-revalidate=600", "Content-Type": "image/jpeg", }, }); } catch (error) { - console.error(`Gagal memproses file: ${name}`, error); - // Jika file tidak ditemukan atau gagal diproses, kembalikan default image + console.error(`Gagal mengambil file dari MinIO: ${name}`, error); return new Response(await fs.readFile(noImage), { headers: { "Content-Type": "image/jpeg" }, }); diff --git a/src/app/api/[[...slugs]]/_lib/imgs.ts b/src/app/api/[[...slugs]]/_lib/imgs.ts index 5b3470f7..fdd439a9 100644 --- a/src/app/api/[[...slugs]]/_lib/imgs.ts +++ b/src/app/api/[[...slugs]]/_lib/imgs.ts @@ -1,30 +1,30 @@ -import fs from "fs/promises"; +import minio, { MINIO_BUCKET } from "@/lib/minio"; async function imgs({ search = "", page = 1, count = 20, - UPLOAD_DIR_IMAGE, }: { search?: string; page?: number; count?: number; - UPLOAD_DIR_IMAGE: string; }) { - const files = await fs.readdir(UPLOAD_DIR_IMAGE); + const objects: { name: string; url: string }[] = []; - return files - .filter( - (file) => - file.endsWith(".jpg") || file.endsWith(".png") || file.endsWith(".jpeg") - ) - .filter((file) => file.includes(search)) + const stream = minio.listObjects(MINIO_BUCKET, "image/", true); + + for await (const obj of stream) { + if (!obj.name) continue; + const fileName = obj.name.replace("image/", ""); + if (!fileName) continue; + if (search && !fileName.includes(search)) continue; + objects.push({ name: fileName, url: `/api/img/${fileName}` }); + } + + const total = objects.length; + return objects .slice((page - 1) * count, page * count) - .map((file) => ({ - name: file, - url: `/api/img/${file}`, - total: files.length, - })); + .map((o) => ({ ...o, total })); } export default imgs; diff --git a/src/app/api/[[...slugs]]/_lib/upl-img-single.ts b/src/app/api/[[...slugs]]/_lib/upl-img-single.ts index a8d7c7a2..a4ef65da 100644 --- a/src/app/api/[[...slugs]]/_lib/upl-img-single.ts +++ b/src/app/api/[[...slugs]]/_lib/upl-img-single.ts @@ -1,30 +1,31 @@ import { nanoid } from "nanoid"; -import fs from "fs/promises"; import path from "path"; import _ from "lodash"; +import minio, { MINIO_BUCKET } from "@/lib/minio"; export async function uplImgSingle({ fileName, file, - UPLOAD_DIR_IMAGE, }: { fileName: string; file: File; - UPLOAD_DIR_IMAGE: string; }) { if (!fileName || typeof fileName !== "string" || fileName.trim() === "") { console.warn(`Nama file tidak valid: ${fileName}`); fileName = nanoid() + ".jpg"; } + const ext = path.extname(fileName).toLowerCase(); const fileNameWithoutExt = path.basename(fileName, ext); const fileNameKebabCase = _.kebabCase(fileNameWithoutExt) + ext; + const objectName = `image/${fileNameKebabCase}`; try { const buffer = Buffer.from(await file.arrayBuffer()); - const filePath = path.join(UPLOAD_DIR_IMAGE, fileNameKebabCase); - await fs.writeFile(filePath, buffer); - return filePath; + await minio.putObject(MINIO_BUCKET, objectName, buffer, buffer.length, { + "Content-Type": file.type || "image/jpeg", + }); + return objectName; } catch (error) { console.log(error); return "error"; diff --git a/src/app/api/[[...slugs]]/_lib/upl-img.ts b/src/app/api/[[...slugs]]/_lib/upl-img.ts index 9fdf482e..de519f4f 100644 --- a/src/app/api/[[...slugs]]/_lib/upl-img.ts +++ b/src/app/api/[[...slugs]]/_lib/upl-img.ts @@ -1,15 +1,11 @@ -import path from "path"; -import fs from "fs/promises"; import { nanoid } from "nanoid"; +import minio, { MINIO_BUCKET } from "@/lib/minio"; -async function uplImg({ - files, - UPLOAD_DIR_IMAGE, -}: { - files: File[]; - UPLOAD_DIR_IMAGE: string; -}) { - // Validasi input +function sanitizeFileName(fileName: string): string { + return fileName.replace(/[^a-zA-Z0-9._\-]/g, "_"); +} + +async function uplImg({ files }: { files: File[] }) { if (!Array.isArray(files) || files.length === 0) { throw new Error("Tidak ada file yang diunggah"); } @@ -17,24 +13,20 @@ async function uplImg({ for (const file of files) { let fileName = file.name; - // Validasi nama file if (!fileName || typeof fileName !== "string" || fileName.trim() === "") { console.warn(`Nama file tidak valid: ${fileName}`); fileName = nanoid() + ".jpg"; } - // Sanitasi nama file untuk mencegah path traversal - const sanitizedFileName = sanitizeFileName(fileName); + const sanitized = sanitizeFileName(fileName); + const objectName = `image/${sanitized}`; try { - // Konversi file ke buffer const buffer = Buffer.from(await file.arrayBuffer()); - - // Tulis file ke direktori uploads - const filePath = path.join(UPLOAD_DIR_IMAGE, sanitizedFileName); - await fs.writeFile(filePath, buffer); - - console.log(`File berhasil diunggah: ${sanitizedFileName}`); + await minio.putObject(MINIO_BUCKET, objectName, buffer, buffer.length, { + "Content-Type": file.type || "image/jpeg", + }); + console.log(`File berhasil diunggah ke MinIO: ${objectName}`); } catch (error) { console.error(`Gagal mengunggah file ${fileName}:`, error); throw new Error(`Gagal mengunggah file: ${fileName}`); @@ -44,9 +36,4 @@ async function uplImg({ return "ok"; } -// Fungsi untuk membersihkan nama file dari karakter yang tidak aman -function sanitizeFileName(fileName: string): string { - return fileName.replace(/[^a-zA-Z0-9._\-]/g, "_"); -} - export default uplImg; diff --git a/src/app/api/[[...slugs]]/route.ts b/src/app/api/[[...slugs]]/route.ts index a84cb826..e8ffd1a2 100644 --- a/src/app/api/[[...slugs]]/route.ts +++ b/src/app/api/[[...slugs]]/route.ts @@ -4,32 +4,15 @@ import swagger from "@elysiajs/swagger"; import { Elysia, t } from "elysia"; import getPotensi from "./_lib/get-potensi"; import img from "./_lib/img"; -import fs from "fs/promises"; -import path from "path"; import uplImg from "./_lib/upl-img"; import imgs from "./_lib/imgs"; import uplCsv from "./_lib/upl-csv"; import imgDel from "./_lib/img-del"; import { uplImgSingle } from "./_lib/upl-img-single"; import { uplCsvSingle } from "./_lib/upl-csv-single"; + const ROOT = process.cwd(); -if (!process.env.WIBU_UPLOAD_DIR) - throw new Error("WIBU_UPLOAD_DIR is not defined"); - -const UPLOAD_DIR = path.join(ROOT, process.env.WIBU_UPLOAD_DIR); -const UPLOAD_DIR_IMAGE = path.join(UPLOAD_DIR, "image"); - -// create uploads dir -fs.mkdir(UPLOAD_DIR, { - recursive: true, -}).catch(() => {}); - -// create image uploads dir -fs.mkdir(UPLOAD_DIR_IMAGE, { - recursive: true, -}).catch(() => {}); - const corsConfig = { origin: "*", methods: ["GET", "POST", "PATCH", "DELETE", "PUT"] as HTTPMethod[], @@ -43,6 +26,7 @@ async function layanan() { const data = await prisma.layanan.findMany(); return { data }; } + const ApiServer = new Elysia() .use(swagger({ path: "/api/docs" })) .use(cors(corsConfig)) @@ -63,7 +47,6 @@ const ApiServer = new Elysia() ({ params, query }) => { return img({ name: params.name, - UPLOAD_DIR_IMAGE, ROOT, size: query.size, }); @@ -82,10 +65,7 @@ const ApiServer = new Elysia() .delete( "/img/:name", ({ params }) => { - return imgDel({ - name: params.name, - UPLOAD_DIR_IMAGE, - }); + return imgDel({ name: params.name }); }, { params: t.Object({ @@ -100,7 +80,6 @@ const ApiServer = new Elysia() search: query.search, page: query.page, count: query.count, - UPLOAD_DIR_IMAGE, }); }, { @@ -117,7 +96,7 @@ const ApiServer = new Elysia() "/upl-img", ({ body }) => { console.log(body.title); - return uplImg({ files: body.files, UPLOAD_DIR_IMAGE }); + return uplImg({ files: body.files }); }, { body: t.Object({ @@ -132,7 +111,6 @@ const ApiServer = new Elysia() return uplImgSingle({ fileName: body.name, file: body.file, - UPLOAD_DIR_IMAGE, }); }, { diff --git a/src/lib/minio.ts b/src/lib/minio.ts new file mode 100644 index 00000000..32712102 --- /dev/null +++ b/src/lib/minio.ts @@ -0,0 +1,12 @@ +import { Client } from "minio"; + +const minioClient = new Client({ + endPoint: process.env.MINIO_ENDPOINT!, + accessKey: process.env.MINIO_ACCESS_KEY!, + secretKey: process.env.MINIO_SECRET_KEY!, + useSSL: process.env.MINIO_USE_SSL === "true", +}); + +export const MINIO_BUCKET = process.env.MINIO_BUCKET!; + +export default minioClient;