diff --git a/bun.lockb b/bun.lockb index 14f3d9e9..afa73367 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index cfab3f92..2cfb90fb 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "bun": "^1.2.2", "chart.js": "^4.4.8", "classnames": "^2.5.1", + "cli-progress": "^3.12.0", "colors": "^1.4.0", "date-fns": "^4.1.0", "dayjs": "^1.11.13", @@ -104,6 +105,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@types/cli-progress": "^3.11.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^20", "@types/react": "^19", diff --git a/prisma/resolveImageByName.ts b/prisma/resolveImageByName.ts index 0d9e0cff..e105eef3 100644 --- a/prisma/resolveImageByName.ts +++ b/prisma/resolveImageByName.ts @@ -1,25 +1,78 @@ +// import prisma from "@/lib/prisma"; + +// // Ganti nama fungsi dan logikanya +// export default async function resolveImageById( +// imageId?: string | null +// ): Promise { +// if (!imageId) return null; + +// const image = await prisma.fileStorage.findFirst({ +// where: { +// id: imageId, // ← cari berdasarkan ID +// category: "image", +// isActive: true, +// deletedAt: null, +// }, +// select: { id: true }, +// }); + +// if (!image) { +// console.warn(`⚠️ Image with ID ${imageId} not found`); +// return null; +// } + +// return image.id; +// } + import prisma from "@/lib/prisma"; -// Ganti nama fungsi dan logikanya -export default async function resolveImageById( - imageId?: string | null -): Promise { +/** + * Resolve image ID by checking multiple possible names + * @param imageId - The ID from JSON (could be filename or actual ID) + * @returns The actual database ID or null + */ +export default async function resolveImageById(imageId: string | null): Promise { if (!imageId) return null; - const image = await prisma.fileStorage.findFirst({ - where: { - id: imageId, // ← cari berdasarkan ID - category: "image", - isActive: true, - deletedAt: null, - }, - select: { id: true }, - }); + try { + // 1. Coba cari berdasarkan ID langsung + const byId = await prisma.fileStorage.findUnique({ + where: { id: imageId }, + select: { id: true }, + }); + if (byId) return byId.id; - if (!image) { - console.warn(`⚠️ Image with ID ${imageId} not found`); + // 2. Coba cari berdasarkan name (exact match) + const byName = await prisma.fileStorage.findUnique({ + where: { name: imageId }, + select: { id: true }, + }); + if (byName) return byName.id; + + // 3. Coba cari berdasarkan realName + const byRealName = await prisma.fileStorage.findFirst({ + where: { realName: imageId }, + select: { id: true }, + }); + if (byRealName) return byRealName.id; + + // 4. Coba dengan menambahkan ekstensi .webp + const withWebp = `${imageId.replace(/\.(jpg|jpeg|png)$/i, '')}.webp`; + const byWebp = await prisma.fileStorage.findFirst({ + where: { + OR: [ + { name: withWebp }, + { name: { contains: imageId.split('.')[0] } }, + ], + }, + select: { id: true }, + }); + if (byWebp) return byWebp.id; + + console.warn(`⚠️ Image not found for: ${imageId}`); + return null; + } catch (error) { + console.error(`❌ Error resolving image ${imageId}:`, error); return null; } - - return image.id; } \ No newline at end of file diff --git a/prisma/safeSeedMany.ts b/prisma/safeSeedMany.ts new file mode 100644 index 00000000..d909bb2c --- /dev/null +++ b/prisma/safeSeedMany.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { PrismaClient } from "@prisma/client"; +import { safeSeedUnique } from "./safeseedUnique"; +import cliProgress from 'cli-progress'; + +type SafeSeedOptions = { + skipUpdate?: boolean; + silent?: boolean; // Opsional: untuk suppress log +}; + +/** + * Batch upsert with progress logging + */ +export async function safeSeedMany( + model: T, + items: Array<{ where: Record; data: Record }>, + options: SafeSeedOptions = {} +) { + const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); + bar.start(items.length, 0); + + let success = 0; + let failed = 0; + let skipped = 0; + + for (const [index, item] of items.entries()) { + try { + const result = await safeSeedUnique(model, item.where, item.data, { + ...options, + silent: true, + }); + if (result) success++; + else skipped++; + } catch (err) { + failed++; + } + bar.update(index + 1); + } + + bar.stop(); + console.log(`✅ ${String(model)}: ${success} seeded, ${skipped} skipped, ${failed} failed`); + + return { success, skipped, failed }; +} \ No newline at end of file diff --git a/prisma/safeseedUnique.ts b/prisma/safeseedUnique.ts index 75f942dd..e0ea0d52 100644 --- a/prisma/safeseedUnique.ts +++ b/prisma/safeseedUnique.ts @@ -4,9 +4,16 @@ import { PrismaClient } from "@prisma/client"; type SafeSeedOptions = { skipUpdate?: boolean; + silent?: boolean; // Opsional: untuk suppress log }; -// prisma/safeseedUnique.ts +/** + * Safely upsert data with error handling + * @param model - Prisma model name + * @param where - Unique identifier(s) + * @param data - Full data object (will be used for create) + * @param options - Additional options + */ export async function safeSeedUnique( model: T, where: Record, @@ -14,23 +21,87 @@ export async function safeSeedUnique( options: SafeSeedOptions = {} ) { const m = prisma[model] as any; - if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`); + + if (!m) { + throw new Error(`❌ Model ${String(model)} tidak ditemukan di Prisma Client`); + } try { - // Pastikan `where` berisi field yang benar-benar unique (misal: `id`) const result = await m.upsert({ where, update: options.skipUpdate ? {} : data, - create: data, // ✅ Jangan duplikasi `where` ke `create` + create: data, }); - console.log(`✅ Seed ${String(model)}:`, where); + + if (!options.silent) { + console.log(`✅ Seeded ${String(model)}:`, where); + } + return result; - } catch (err) { - console.error(`❌ Gagal seed ${String(model)}:`, where, err); - throw err; // ✅ Rethrow agar seeding berhenti jika kritis + } catch (err: any) { + // Handle specific Prisma errors + if (err.code === "P2002") { + console.warn(`⚠️ Duplicate ${String(model)} (skipped):`, where); + return null; + } + + if (err.code === "P2003") { + console.error(`❌ Foreign key constraint failed for ${String(model)}:`, where); + console.error(" Missing relation:", err.meta?.field_name); + throw err; + } + + if (err.code === "P2025") { + console.error(`❌ Record not found for ${String(model)}:`, where); + throw err; + } + + // Log unexpected errors with full details + console.error(`❌ Failed to seed ${String(model)}:`, where); + console.error(" Error:", err.message); + console.error(" Code:", err.code); + + throw err; } } + + +//ini yang bener pertama + +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// import prisma from "@/lib/prisma"; +// import { PrismaClient } from "@prisma/client"; + +// type SafeSeedOptions = { +// skipUpdate?: boolean; +// }; + +// // prisma/safeseedUnique.ts +// export async function safeSeedUnique( +// model: T, +// where: Record, +// data: Record, +// options: SafeSeedOptions = {} +// ) { +// const m = prisma[model] as any; +// if (!m) throw new Error(`Model ${String(model)} tidak ditemukan`); + +// try { +// // Pastikan `where` berisi field yang benar-benar unique (misal: `id`) +// const result = await m.upsert({ +// where, +// update: options.skipUpdate ? {} : data, +// create: data, // ✅ Jangan duplikasi `where` ke `create` +// }); +// console.log(`✅ Seed ${String(model)}:`, where); +// return result; +// } catch (err) { +// console.error(`❌ Gagal seed ${String(model)}:`, where, err); +// throw err; // ✅ Rethrow agar seeding berhenti jika kritis +// } +// } + // /* eslint-disable @typescript-eslint/no-explicit-any */ // import { PrismaClient } from "@prisma/client"; diff --git a/prisma/seed.ts b/prisma/seed.ts index 3772fb42..129643d5 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -3018,6 +3018,7 @@ import resolveImageById from "./resolveImageByName"; } console.log("✅ Data perpustakaan seeded successfully"); + // =========== SUBMENU DATA PENDIDIKAN ===================== console.log("🔄 Seeding Data pendidikan..."); for (const k of dataPendidikan) { await prisma.dataPendidikan.upsert({ @@ -3037,7 +3038,6 @@ import resolveImageById from "./resolveImageByName"; } console.log("✅ Data pendidikan seeded successfully"); - // =========== SUBMENU DATA PENDIDIKAN ===================== })() .then(() => prisma.$disconnect()) .catch((e) => { diff --git a/prisma/seed_assets.ts b/prisma/seed_assets.ts index 1565c034..1bc230da 100644 --- a/prisma/seed_assets.ts +++ b/prisma/seed_assets.ts @@ -1,218 +1,27 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -// prisma/seedAssets.ts -import prisma from "@/lib/prisma"; -import AdmZip from "adm-zip"; -import fs from "fs/promises"; -import path from "path"; -import sharp from "sharp"; -import fetchWithRetry from "./data/fetchWithRetry"; - -const UPLOADS_DIR = path.resolve(process.env.WIBU_UPLOAD_DIR || "uploads"); - -// --- Helper: deteksi kategori file --- -function detectCategory(filename: string): "image" | "document" | "other" { - const ext = path.extname(filename).toLowerCase(); - if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image"; - if ([".pdf", ".doc", ".docx"].includes(ext)) return "document"; - return "other"; -} - -// --- Helper: recursive walk dir --- -async function walkDir( - dir: string, - fileList: string[] = [], -): Promise { - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - if (entry.name === "__MACOSX") continue; // skip folder sampah - await walkDir(fullPath, fileList); - } else { - if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; // skip file sampah - fileList.push(fullPath); - } - } - - return fileList; -} - -export default async function seedAssets() { - console.log("🚀 Seeding assets..."); - console.log("📁 Upload dir:", UPLOADS_DIR); - - await fs.mkdir(UPLOADS_DIR, { recursive: true }); - - // 1. Download zip - const url = - "https://cld-dkr-makuro-seafile.wibudev.com/f/03be4043989e4caeb36b/?dl=1"; - const res = await fetchWithRetry(url, 3, 20000); - - // Validasi content-type - const contentType = res.headers.get("content-type"); - if (!contentType?.includes("zip")) { - throw new Error(`Invalid content-type (${contentType}). Expected ZIP file`); - } - - const buffer = Buffer.from(await res.arrayBuffer()); - - // Validasi ukuran file - if (buffer.length < 100) { - throw new Error("Downloaded ZIP is empty or corrupted"); - } - - // Validasi signature ZIP ("PK") - if (buffer.toString("utf8", 0, 2) !== "PK") { - throw new Error("Invalid ZIP signature (PK not found)"); - } - - // 2. Extract zip ke folder tmp - const extractDir = path.join(process.cwd(), "tmp_assets"); - await fs.rm(extractDir, { recursive: true, force: true }); - await fs.mkdir(extractDir, { recursive: true }); - - let zip: AdmZip; - - try { - zip = new AdmZip(buffer); - } catch (err) { - throw new Error("Failed to parse ZIP file (corrupted or invalid)"); - } - - try { - zip.extractAllTo(extractDir, true); - } catch (err) { - throw new Error("Failed to extract ZIP contents"); - } - - // 3. Cari semua file valid (recursive) - const files = await walkDir(extractDir); - - // 4. Loop tiap file & simpan - for (const filePath of files) { - const entryName = path.basename(filePath); - const category = detectCategory(entryName); - - let finalName = entryName; - let mimeType = "application/octet-stream"; - let targetPath = ""; - - if (category === "image") { - const fileBaseName = path.parse(entryName).name; - finalName = `${fileBaseName}.webp`; - targetPath = path.join(UPLOADS_DIR, "images", finalName); - await fs.mkdir(path.dirname(targetPath), { recursive: true }); - await sharp(filePath).webp({ quality: 80 }).toFile(targetPath); - mimeType = "image/webp"; - } else if (category === "document") { - targetPath = path.join(UPLOADS_DIR, "documents", entryName); - await fs.mkdir(path.dirname(targetPath), { recursive: true }); - await fs.copyFile(filePath, targetPath); - mimeType = "application/pdf"; - } else { - targetPath = path.join(UPLOADS_DIR, "other", entryName); - await fs.mkdir(path.dirname(targetPath), { recursive: true }); - await fs.copyFile(filePath, targetPath); - } - - const existing = await prisma.fileStorage.findUnique({ - where: { name: finalName }, - }); - - if (existing) { - // Restore kalau soft deleted - await prisma.fileStorage.update({ - where: { name: finalName }, - data: { - path: targetPath, - realName: entryName, - mimeType, - link: `/uploads/${category}/${finalName}`, - category, - deletedAt: null, - isActive: true, - }, - }); - - console.log(`♻️ restored: ${category}/${finalName}`); - } else { - await prisma.fileStorage.create({ - data: { - name: finalName, - realName: entryName, - path: targetPath, - mimeType, - link: `/uploads/${category}/${finalName}`, - category, - }, - }); - - console.log(`📂 created: ${category}/${finalName}`); - } - - console.log(`📂 saved: ${category}/${finalName}`); - } - - // 6. Cleanup - await fs.rm(extractDir, { recursive: true, force: true }); - - console.log("✅ Selesai seed assets!"); - console.log("DB URL (asset):", process.env.DATABASE_URL); -} - -// --- Auto run kalau dipanggil langsung --- -if (import.meta.main) { - seedAssets() - .catch((err) => { - console.error("❌ Error seeding assets:", err); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); -} - -// prisma/seedAssets.ts +// /* eslint-disable @typescript-eslint/no-unused-vars */ +// // prisma/seedAssets.ts // import prisma from "@/lib/prisma"; // import AdmZip from "adm-zip"; // import fs from "fs/promises"; // import path from "path"; // import sharp from "sharp"; -// import mime from "mime-types"; // import fetchWithRetry from "./data/fetchWithRetry"; -// /* ========================= -// * CONFIG -// * ========================= */ -// const UPLOADS_DIR = path.resolve( -// process.env.WIBU_UPLOAD_DIR || "uploads" -// ); +// const UPLOADS_DIR = path.resolve(process.env.WIBU_UPLOAD_DIR || "uploads"); -// const TMP_DIR = path.join(process.cwd(), "tmp_assets"); - -// const CATEGORY_DIR: Record = { -// image: "images", -// document: "documents", -// other: "other", -// }; - -// type FileCategory = "image" | "document" | "other"; - -// /* ========================= -// * HELPERS -// * ========================= */ -// function detectCategory(filename: string): FileCategory { +// // --- Helper: deteksi kategori file --- +// function detectCategory(filename: string): "image" | "document" | "other" { // const ext = path.extname(filename).toLowerCase(); // if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image"; -// if ([".pdf", ".doc", ".docx", ".txt"].includes(ext)) return "document"; +// if ([".pdf", ".doc", ".docx"].includes(ext)) return "document"; // return "other"; // } +// // --- Helper: recursive walk dir --- // async function walkDir( // dir: string, -// result: string[] = [] +// fileList: string[] = [], // ): Promise { // const entries = await fs.readdir(dir, { withFileTypes: true }); @@ -220,139 +29,141 @@ if (import.meta.main) { // const fullPath = path.join(dir, entry.name); // if (entry.isDirectory()) { -// if (entry.name === "__MACOSX") continue; -// await walkDir(fullPath, result); +// if (entry.name === "__MACOSX") continue; // skip folder sampah +// await walkDir(fullPath, fileList); // } else { -// if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; -// result.push(fullPath); +// if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; // skip file sampah +// fileList.push(fullPath); // } // } -// return result; +// return fileList; // } -// async function ensureDir(dir: string) { -// await fs.mkdir(dir, { recursive: true }); -// } - -// /* ========================= -// * FILE PROCESSORS -// * ========================= */ -// async function processImage(filePath: string, entryName: string) { -// const baseName = path.parse(entryName).name; -// const finalName = `${baseName}.webp`; -// const targetDir = path.join(UPLOADS_DIR, CATEGORY_DIR.image); -// const targetPath = path.join(targetDir, finalName); - -// await ensureDir(targetDir); -// await sharp(filePath).webp({ quality: 80 }).toFile(targetPath); - -// return { -// finalName, -// targetPath, -// mimeType: "image/webp", -// }; -// } - -// async function processNonImage( -// filePath: string, -// entryName: string, -// category: FileCategory -// ) { -// const targetDir = path.join(UPLOADS_DIR, CATEGORY_DIR[category]); -// const targetPath = path.join(targetDir, entryName); - -// await ensureDir(targetDir); -// await fs.copyFile(filePath, targetPath); - -// return { -// finalName: entryName, -// targetPath, -// mimeType: mime.lookup(entryName) || "application/octet-stream", -// }; -// } - -// /* ========================= -// * MAIN -// * ========================= */ // export default async function seedAssets() { // console.log("🚀 Seeding assets..."); // console.log("📁 Upload dir:", UPLOADS_DIR); -// await ensureDir(UPLOADS_DIR); +// await fs.mkdir(UPLOADS_DIR, { recursive: true }); -// /* ===== Download ZIP ===== */ +// // 1. Download zip // const url = -// "https://cld-dkr-makuro-seafile.wibudev.com/f/e13d5429785640c098ae/?dl=1"; +// "https://cld-dkr-makuro-seafile.wibudev.com/f/03be4043989e4caeb36b/?dl=1"; // const res = await fetchWithRetry(url, 3, 20000); -// if (!res.headers.get("content-type")?.includes("zip")) { -// throw new Error("Invalid ZIP content-type"); +// // Validasi content-type +// const contentType = res.headers.get("content-type"); +// if (!contentType?.includes("zip")) { +// throw new Error(`Invalid content-type (${contentType}). Expected ZIP file`); // } // const buffer = Buffer.from(await res.arrayBuffer()); -// if (buffer.length < 100 || buffer.toString("utf8", 0, 2) !== "PK") { -// throw new Error("Corrupted ZIP file"); + +// // Validasi ukuran file +// if (buffer.length < 100) { +// throw new Error("Downloaded ZIP is empty or corrupted"); // } -// /* ===== Extract ===== */ -// await fs.rm(TMP_DIR, { recursive: true, force: true }); -// await ensureDir(TMP_DIR); +// // Validasi signature ZIP ("PK") +// if (buffer.toString("utf8", 0, 2) !== "PK") { +// throw new Error("Invalid ZIP signature (PK not found)"); +// } -// const zip = new AdmZip(buffer); -// zip.extractAllTo(TMP_DIR, true); +// // 2. Extract zip ke folder tmp +// const extractDir = path.join(process.cwd(), "tmp_assets"); +// await fs.rm(extractDir, { recursive: true, force: true }); +// await fs.mkdir(extractDir, { recursive: true }); -// /* ===== Process Files ===== */ -// const files = await walkDir(TMP_DIR); +// let zip: AdmZip; +// try { +// zip = new AdmZip(buffer); +// } catch (err) { +// throw new Error("Failed to parse ZIP file (corrupted or invalid)"); +// } + +// try { +// zip.extractAllTo(extractDir, true); +// } catch (err) { +// throw new Error("Failed to extract ZIP contents"); +// } + +// // 3. Cari semua file valid (recursive) +// const files = await walkDir(extractDir); + +// // 4. Loop tiap file & simpan // for (const filePath of files) { // const entryName = path.basename(filePath); // const category = detectCategory(entryName); -// let result; +// let finalName = entryName; +// let mimeType = "application/octet-stream"; +// let targetPath = ""; // if (category === "image") { -// result = await processImage(filePath, entryName); +// const fileBaseName = path.parse(entryName).name; +// finalName = `${fileBaseName}.webp`; +// targetPath = path.join(UPLOADS_DIR, "images", finalName); +// await fs.mkdir(path.dirname(targetPath), { recursive: true }); +// await sharp(filePath).webp({ quality: 80 }).toFile(targetPath); +// mimeType = "image/webp"; +// } else if (category === "document") { +// targetPath = path.join(UPLOADS_DIR, "documents", entryName); +// await fs.mkdir(path.dirname(targetPath), { recursive: true }); +// await fs.copyFile(filePath, targetPath); +// mimeType = "application/pdf"; // } else { -// result = await processNonImage(filePath, entryName, category); +// targetPath = path.join(UPLOADS_DIR, "other", entryName); +// await fs.mkdir(path.dirname(targetPath), { recursive: true }); +// await fs.copyFile(filePath, targetPath); // } -// const { finalName, targetPath, mimeType } = result; - // const existing = await prisma.fileStorage.findUnique({ // where: { name: finalName }, // }); -// const data = { -// name: finalName, -// realName: entryName, -// path: targetPath, -// mimeType, -// link: `/uploads/${CATEGORY_DIR[category]}/${finalName}`, -// category, -// deletedAt: null, -// isActive: true, -// }; - // if (existing) { +// // Restore kalau soft deleted // await prisma.fileStorage.update({ // where: { name: finalName }, -// data, +// data: { +// path: targetPath, +// realName: entryName, +// mimeType, +// link: `/uploads/${category}/${finalName}`, +// category, +// deletedAt: null, +// isActive: true, +// }, // }); + // console.log(`♻️ restored: ${category}/${finalName}`); // } else { -// await prisma.fileStorage.create({ data }); +// await prisma.fileStorage.create({ +// data: { +// name: finalName, +// realName: entryName, +// path: targetPath, +// mimeType, +// link: `/uploads/${category}/${finalName}`, +// category, +// }, +// }); + // console.log(`📂 created: ${category}/${finalName}`); // } + +// console.log(`📂 saved: ${category}/${finalName}`); // } -// /* ===== Cleanup ===== */ -// await fs.rm(TMP_DIR, { recursive: true, force: true }); +// // 6. Cleanup +// await fs.rm(extractDir, { recursive: true, force: true }); // console.log("✅ Selesai seed assets!"); +// console.log("DB URL (asset):", process.env.DATABASE_URL); // } -// /* ===== Auto Run ===== */ +// // --- Auto run kalau dipanggil langsung --- // if (import.meta.main) { // seedAssets() // .catch((err) => { @@ -363,3 +174,382 @@ if (import.meta.main) { // await prisma.$disconnect(); // }); // } + +// // prisma/seedAssets.ts +// // import prisma from "@/lib/prisma"; +// // import AdmZip from "adm-zip"; +// // import fs from "fs/promises"; +// // import path from "path"; +// // import sharp from "sharp"; +// // import mime from "mime-types"; +// // import fetchWithRetry from "./data/fetchWithRetry"; + +// // /* ========================= +// // * CONFIG +// // * ========================= */ +// // const UPLOADS_DIR = path.resolve( +// // process.env.WIBU_UPLOAD_DIR || "uploads" +// // ); + +// // const TMP_DIR = path.join(process.cwd(), "tmp_assets"); + +// // const CATEGORY_DIR: Record = { +// // image: "images", +// // document: "documents", +// // other: "other", +// // }; + +// // type FileCategory = "image" | "document" | "other"; + +// // /* ========================= +// // * HELPERS +// // * ========================= */ +// // function detectCategory(filename: string): FileCategory { +// // const ext = path.extname(filename).toLowerCase(); +// // if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image"; +// // if ([".pdf", ".doc", ".docx", ".txt"].includes(ext)) return "document"; +// // return "other"; +// // } + +// // async function walkDir( +// // dir: string, +// // result: string[] = [] +// // ): Promise { +// // const entries = await fs.readdir(dir, { withFileTypes: true }); + +// // for (const entry of entries) { +// // const fullPath = path.join(dir, entry.name); + +// // if (entry.isDirectory()) { +// // if (entry.name === "__MACOSX") continue; +// // await walkDir(fullPath, result); +// // } else { +// // if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; +// // result.push(fullPath); +// // } +// // } + +// // return result; +// // } + +// // async function ensureDir(dir: string) { +// // await fs.mkdir(dir, { recursive: true }); +// // } + +// // /* ========================= +// // * FILE PROCESSORS +// // * ========================= */ +// // async function processImage(filePath: string, entryName: string) { +// // const baseName = path.parse(entryName).name; +// // const finalName = `${baseName}.webp`; +// // const targetDir = path.join(UPLOADS_DIR, CATEGORY_DIR.image); +// // const targetPath = path.join(targetDir, finalName); + +// // await ensureDir(targetDir); +// // await sharp(filePath).webp({ quality: 80 }).toFile(targetPath); + +// // return { +// // finalName, +// // targetPath, +// // mimeType: "image/webp", +// // }; +// // } + +// // async function processNonImage( +// // filePath: string, +// // entryName: string, +// // category: FileCategory +// // ) { +// // const targetDir = path.join(UPLOADS_DIR, CATEGORY_DIR[category]); +// // const targetPath = path.join(targetDir, entryName); + +// // await ensureDir(targetDir); +// // await fs.copyFile(filePath, targetPath); + +// // return { +// // finalName: entryName, +// // targetPath, +// // mimeType: mime.lookup(entryName) || "application/octet-stream", +// // }; +// // } + +// // /* ========================= +// // * MAIN +// // * ========================= */ +// // export default async function seedAssets() { +// // console.log("🚀 Seeding assets..."); +// // console.log("📁 Upload dir:", UPLOADS_DIR); + +// // await ensureDir(UPLOADS_DIR); + +// // /* ===== Download ZIP ===== */ +// // const url = +// // "https://cld-dkr-makuro-seafile.wibudev.com/f/e13d5429785640c098ae/?dl=1"; +// // const res = await fetchWithRetry(url, 3, 20000); + +// // if (!res.headers.get("content-type")?.includes("zip")) { +// // throw new Error("Invalid ZIP content-type"); +// // } + +// // const buffer = Buffer.from(await res.arrayBuffer()); +// // if (buffer.length < 100 || buffer.toString("utf8", 0, 2) !== "PK") { +// // throw new Error("Corrupted ZIP file"); +// // } + +// // /* ===== Extract ===== */ +// // await fs.rm(TMP_DIR, { recursive: true, force: true }); +// // await ensureDir(TMP_DIR); + +// // const zip = new AdmZip(buffer); +// // zip.extractAllTo(TMP_DIR, true); + +// // /* ===== Process Files ===== */ +// // const files = await walkDir(TMP_DIR); + +// // for (const filePath of files) { +// // const entryName = path.basename(filePath); +// // const category = detectCategory(entryName); + +// // let result; + +// // if (category === "image") { +// // result = await processImage(filePath, entryName); +// // } else { +// // result = await processNonImage(filePath, entryName, category); +// // } + +// // const { finalName, targetPath, mimeType } = result; + +// // const existing = await prisma.fileStorage.findUnique({ +// // where: { name: finalName }, +// // }); + +// // const data = { +// // name: finalName, +// // realName: entryName, +// // path: targetPath, +// // mimeType, +// // link: `/uploads/${CATEGORY_DIR[category]}/${finalName}`, +// // category, +// // deletedAt: null, +// // isActive: true, +// // }; + +// // if (existing) { +// // await prisma.fileStorage.update({ +// // where: { name: finalName }, +// // data, +// // }); +// // console.log(`♻️ restored: ${category}/${finalName}`); +// // } else { +// // await prisma.fileStorage.create({ data }); +// // console.log(`📂 created: ${category}/${finalName}`); +// // } +// // } + +// // /* ===== Cleanup ===== */ +// // await fs.rm(TMP_DIR, { recursive: true, force: true }); + +// // console.log("✅ Selesai seed assets!"); +// // } + +// // /* ===== Auto Run ===== */ +// // if (import.meta.main) { +// // seedAssets() +// // .catch((err) => { +// // console.error("❌ Error seeding assets:", err); +// // process.exit(1); +// // }) +// // .finally(async () => { +// // await prisma.$disconnect(); +// // }); +// // } + +import prisma from "@/lib/prisma"; +import AdmZip from "adm-zip"; +import fs from "fs/promises"; +import path from "path"; +import sharp from "sharp"; +import fetchWithRetry from "./data/fetchWithRetry"; +import { constants } from "fs"; + +// ✅ Gunakan env variable dengan fallback +const UPLOADS_DIR = path.join(process.cwd(), process.env.WIBU_UPLOAD_DIR || "uploads"); + + +function detectCategory(filename: string): "image" | "document" | "other" { + const ext = path.extname(filename).toLowerCase(); + if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image"; + if ([".pdf", ".doc", ".docx"].includes(ext)) return "document"; + return "other"; +} + +async function walkDir( + dir: string, + fileList: string[] = [], +): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === "__MACOSX") continue; + await walkDir(fullPath, fileList); + } else { + if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; + fileList.push(fullPath); + } + } + return fileList; +} + +export default async function seedAssets() { + console.log("🚀 Seeding assets..."); + console.log("📁 Upload dir:", UPLOADS_DIR); + + try { + await fs.access(UPLOADS_DIR, fs.constants.W_OK); + } catch (err) { + console.error("❌ Upload directory is not writable:", UPLOADS_DIR); + throw new Error( + `UPLOADS_DIR not writable: ${UPLOADS_DIR}. Check Docker volume or permissions` + ); + } + + // ✅ Pastikan folder exist + await fs.mkdir(UPLOADS_DIR, { recursive: true }); + try { + await fs.access(UPLOADS_DIR, constants.W_OK); +} catch { + throw new Error( + `UPLOADS_DIR not writable: ${UPLOADS_DIR}. Check Docker volume or permissions` + ); +} + await fs.mkdir(path.join(UPLOADS_DIR, "images"), { recursive: true }); + await fs.mkdir(path.join(UPLOADS_DIR, "documents"), { recursive: true }); + await fs.mkdir(path.join(UPLOADS_DIR, "other"), { recursive: true }); + + const url = + "https://cld-dkr-makuro-seafile.wibudev.com/f/8e9e42e9f3e94c80919e/?dl=1"; + + let buffer: Buffer; + try { + console.log("⬇️ Downloading ZIP from:", url); + const res = await fetchWithRetry(url, 3, 20000); + + const contentType = res.headers.get("content-type"); + if ( + !contentType?.includes("zip") && + !contentType?.includes("octet-stream") + ) { + throw new Error( + `Invalid content-type (${contentType}). Expected ZIP file`, + ); + } + + buffer = Buffer.from(await res.arrayBuffer()); + + if (buffer.length < 100) { + throw new Error("Downloaded ZIP is empty or corrupted"); + } + + if (buffer.toString("utf8", 0, 2) !== "PK") { + throw new Error("Invalid ZIP signature (PK not found)"); + } + + console.log(`✅ Downloaded ${(buffer.length / 1024 / 1024).toFixed(2)} MB`); + } catch (err) { + console.error("❌ Failed to download ZIP:", err); + throw err; + } + + // Extract ZIP + const extractDir = path.join(process.cwd(), "tmp_assets"); + await fs.rm(extractDir, { recursive: true, force: true }); + await fs.mkdir(extractDir, { recursive: true }); + + let zip: AdmZip; + try { + zip = new AdmZip(buffer); + zip.extractAllTo(extractDir, true); + console.log("✅ ZIP extracted successfully"); + } catch (err) { + console.error("❌ Failed to extract ZIP:", err); + throw err; + } + + const files = await walkDir(extractDir); + console.log(`📦 Found ${files.length} files to process`); + + // Process files + for (const filePath of files) { + const entryName = path.basename(filePath); + const category = detectCategory(entryName); + + let finalName = entryName; + let mimeType = "application/octet-stream"; + let targetPath = ""; + + try { + if (category === "image") { + const fileBaseName = path.parse(entryName).name; + finalName = `${fileBaseName}.webp`; + targetPath = path.join(UPLOADS_DIR, "images", finalName); + + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await sharp(filePath).webp({ quality: 80 }).toFile(targetPath); + mimeType = "image/webp"; + } else if (category === "document") { + targetPath = path.join(UPLOADS_DIR, "documents", entryName); + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await fs.copyFile(filePath, targetPath); + mimeType = "application/pdf"; + } else { + targetPath = path.join(UPLOADS_DIR, "other", entryName); + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await fs.copyFile(filePath, targetPath); + } + + // ✅ Upsert ke database + await prisma.fileStorage.upsert({ + where: { name: finalName }, + update: { + path: path.dirname(targetPath), + realName: entryName, + mimeType, + link: `/api/fileStorage/findUnique/${finalName}`, + category, + deletedAt: null, + isActive: true, + }, + create: { + name: finalName, + realName: entryName, + path: path.dirname(targetPath), + mimeType, + link: `/api/fileStorage/findUnique/${finalName}`, + category, + }, + }); + + console.log(`✅ Processed: ${category}/${finalName}`); + } catch (err) { + console.error(`❌ Failed to process ${entryName}`, err); + throw err; // ⛔ penting + } + } + + // Cleanup + await fs.rm(extractDir, { recursive: true, force: true }); + console.log("✅ Asset seeding completed!"); +} + +if (import.meta.main) { + seedAssets() + .catch((err) => { + console.error("❌ Error seeding assets:", err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); +} diff --git a/src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx b/src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx index 2e65b465..8b3fa242 100644 --- a/src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/desa/galery/video/[id]/page.tsx @@ -1,7 +1,9 @@ 'use client'; +import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; import colors from '@/con/colors'; import { + ActionIcon, Alert, Box, Button, @@ -15,12 +17,10 @@ import { Title, } from '@mantine/core'; import { useShallowEffect } from '@mantine/hooks'; -import { IconArrowBack, IconInfoCircle, IconVideo } from '@tabler/icons-react'; +import { IconArrowBack, IconArrowLeft, IconInfoCircle, IconVideo } from '@tabler/icons-react'; import { useParams, useRouter } from 'next/navigation'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; -import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery'; -import BackButton from '../../../layanan/_com/BackButto'; function convertToEmbedUrl(youtubeUrl: string): string { @@ -95,7 +95,9 @@ export default function DetailVideoUser() { {/* Tombol Kembali */} - + router.push('/darmasaba/desa/galery/video')}> + + {/* Header - Dijadikan Title */}