Fix Seed Image 27 Jan #55
@@ -54,6 +54,7 @@
|
|||||||
"bun": "^1.2.2",
|
"bun": "^1.2.2",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
@@ -104,6 +105,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@types/cli-progress": "^3.11.6",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
@@ -1,25 +1,78 @@
|
|||||||
|
// import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
// // Ganti nama fungsi dan logikanya
|
||||||
|
// export default async function resolveImageById(
|
||||||
|
// imageId?: string | null
|
||||||
|
// ): Promise<string | null> {
|
||||||
|
// 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";
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
// Ganti nama fungsi dan logikanya
|
/**
|
||||||
export default async function resolveImageById(
|
* Resolve image ID by checking multiple possible names
|
||||||
imageId?: string | null
|
* @param imageId - The ID from JSON (could be filename or actual ID)
|
||||||
): Promise<string | null> {
|
* @returns The actual database ID or null
|
||||||
|
*/
|
||||||
|
export default async function resolveImageById(imageId: string | null): Promise<string | null> {
|
||||||
if (!imageId) return null;
|
if (!imageId) return null;
|
||||||
|
|
||||||
const image = await prisma.fileStorage.findFirst({
|
try {
|
||||||
where: {
|
// 1. Coba cari berdasarkan ID langsung
|
||||||
id: imageId, // ← cari berdasarkan ID
|
const byId = await prisma.fileStorage.findUnique({
|
||||||
category: "image",
|
where: { id: imageId },
|
||||||
isActive: true,
|
select: { id: true },
|
||||||
deletedAt: null,
|
});
|
||||||
},
|
if (byId) return byId.id;
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
// 2. Coba cari berdasarkan name (exact match)
|
||||||
console.warn(`⚠️ Image with ID ${imageId} not found`);
|
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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return image.id;
|
|
||||||
}
|
}
|
||||||
45
prisma/safeSeedMany.ts
Normal file
45
prisma/safeSeedMany.ts
Normal file
@@ -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<T extends keyof PrismaClient>(
|
||||||
|
model: T,
|
||||||
|
items: Array<{ where: Record<string, any>; data: Record<string, any> }>,
|
||||||
|
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 };
|
||||||
|
}
|
||||||
@@ -4,9 +4,16 @@ import { PrismaClient } from "@prisma/client";
|
|||||||
|
|
||||||
type SafeSeedOptions = {
|
type SafeSeedOptions = {
|
||||||
skipUpdate?: boolean;
|
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<T extends keyof PrismaClient>(
|
export async function safeSeedUnique<T extends keyof PrismaClient>(
|
||||||
model: T,
|
model: T,
|
||||||
where: Record<string, any>,
|
where: Record<string, any>,
|
||||||
@@ -14,23 +21,87 @@ export async function safeSeedUnique<T extends keyof PrismaClient>(
|
|||||||
options: SafeSeedOptions = {}
|
options: SafeSeedOptions = {}
|
||||||
) {
|
) {
|
||||||
const m = prisma[model] as any;
|
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 {
|
try {
|
||||||
// Pastikan `where` berisi field yang benar-benar unique (misal: `id`)
|
|
||||||
const result = await m.upsert({
|
const result = await m.upsert({
|
||||||
where,
|
where,
|
||||||
update: options.skipUpdate ? {} : data,
|
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;
|
return result;
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(`❌ Gagal seed ${String(model)}:`, where, err);
|
// Handle specific Prisma errors
|
||||||
throw err; // ✅ Rethrow agar seeding berhenti jika kritis
|
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<T extends keyof PrismaClient>(
|
||||||
|
// model: T,
|
||||||
|
// where: Record<string, any>,
|
||||||
|
// data: Record<string, any>,
|
||||||
|
// 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 */
|
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
// import { PrismaClient } from "@prisma/client";
|
// import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
|||||||
@@ -3018,6 +3018,7 @@ import resolveImageById from "./resolveImageByName";
|
|||||||
}
|
}
|
||||||
console.log("✅ Data perpustakaan seeded successfully");
|
console.log("✅ Data perpustakaan seeded successfully");
|
||||||
|
|
||||||
|
// =========== SUBMENU DATA PENDIDIKAN =====================
|
||||||
console.log("🔄 Seeding Data pendidikan...");
|
console.log("🔄 Seeding Data pendidikan...");
|
||||||
for (const k of dataPendidikan) {
|
for (const k of dataPendidikan) {
|
||||||
await prisma.dataPendidikan.upsert({
|
await prisma.dataPendidikan.upsert({
|
||||||
@@ -3037,7 +3038,6 @@ import resolveImageById from "./resolveImageByName";
|
|||||||
}
|
}
|
||||||
console.log("✅ Data pendidikan seeded successfully");
|
console.log("✅ Data pendidikan seeded successfully");
|
||||||
|
|
||||||
// =========== SUBMENU DATA PENDIDIKAN =====================
|
|
||||||
})()
|
})()
|
||||||
.then(() => prisma.$disconnect())
|
.then(() => prisma.$disconnect())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@@ -1,218 +1,27 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
// prisma/seedAssets.ts
|
// /* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import prisma from "@/lib/prisma";
|
// // prisma/seedAssets.ts
|
||||||
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<string[]> {
|
|
||||||
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
|
|
||||||
// import prisma from "@/lib/prisma";
|
// import prisma from "@/lib/prisma";
|
||||||
// import AdmZip from "adm-zip";
|
// import AdmZip from "adm-zip";
|
||||||
// import fs from "fs/promises";
|
// import fs from "fs/promises";
|
||||||
// import path from "path";
|
// import path from "path";
|
||||||
// import sharp from "sharp";
|
// import sharp from "sharp";
|
||||||
// import mime from "mime-types";
|
|
||||||
// import fetchWithRetry from "./data/fetchWithRetry";
|
// import fetchWithRetry from "./data/fetchWithRetry";
|
||||||
|
|
||||||
// /* =========================
|
// const UPLOADS_DIR = path.resolve(process.env.WIBU_UPLOAD_DIR || "uploads");
|
||||||
// * CONFIG
|
|
||||||
// * ========================= */
|
|
||||||
// const UPLOADS_DIR = path.resolve(
|
|
||||||
// process.env.WIBU_UPLOAD_DIR || "uploads"
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const TMP_DIR = path.join(process.cwd(), "tmp_assets");
|
// // --- Helper: deteksi kategori file ---
|
||||||
|
// function detectCategory(filename: string): "image" | "document" | "other" {
|
||||||
// const CATEGORY_DIR: Record<FileCategory, string> = {
|
|
||||||
// image: "images",
|
|
||||||
// document: "documents",
|
|
||||||
// other: "other",
|
|
||||||
// };
|
|
||||||
|
|
||||||
// type FileCategory = "image" | "document" | "other";
|
|
||||||
|
|
||||||
// /* =========================
|
|
||||||
// * HELPERS
|
|
||||||
// * ========================= */
|
|
||||||
// function detectCategory(filename: string): FileCategory {
|
|
||||||
// const ext = path.extname(filename).toLowerCase();
|
// const ext = path.extname(filename).toLowerCase();
|
||||||
// if ([".jpg", ".jpeg", ".png", ".webp"].includes(ext)) return "image";
|
// 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";
|
// return "other";
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// // --- Helper: recursive walk dir ---
|
||||||
// async function walkDir(
|
// async function walkDir(
|
||||||
// dir: string,
|
// dir: string,
|
||||||
// result: string[] = []
|
// fileList: string[] = [],
|
||||||
// ): Promise<string[]> {
|
// ): Promise<string[]> {
|
||||||
// const entries = await fs.readdir(dir, { withFileTypes: true });
|
// const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
@@ -220,139 +29,141 @@ if (import.meta.main) {
|
|||||||
// const fullPath = path.join(dir, entry.name);
|
// const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
// if (entry.isDirectory()) {
|
// if (entry.isDirectory()) {
|
||||||
// if (entry.name === "__MACOSX") continue;
|
// if (entry.name === "__MACOSX") continue; // skip folder sampah
|
||||||
// await walkDir(fullPath, result);
|
// await walkDir(fullPath, fileList);
|
||||||
// } else {
|
// } else {
|
||||||
// if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue;
|
// if (entry.name.startsWith(".") || entry.name === ".DS_Store") continue; // skip file sampah
|
||||||
// result.push(fullPath);
|
// 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() {
|
// export default async function seedAssets() {
|
||||||
// console.log("🚀 Seeding assets...");
|
// console.log("🚀 Seeding assets...");
|
||||||
// console.log("📁 Upload dir:", UPLOADS_DIR);
|
// console.log("📁 Upload dir:", UPLOADS_DIR);
|
||||||
|
|
||||||
// await ensureDir(UPLOADS_DIR);
|
// await fs.mkdir(UPLOADS_DIR, { recursive: true });
|
||||||
|
|
||||||
// /* ===== Download ZIP ===== */
|
// // 1. Download zip
|
||||||
// const url =
|
// 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);
|
// const res = await fetchWithRetry(url, 3, 20000);
|
||||||
|
|
||||||
// if (!res.headers.get("content-type")?.includes("zip")) {
|
// // Validasi content-type
|
||||||
// throw new Error("Invalid ZIP 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());
|
// 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 ===== */
|
// // Validasi signature ZIP ("PK")
|
||||||
// await fs.rm(TMP_DIR, { recursive: true, force: true });
|
// if (buffer.toString("utf8", 0, 2) !== "PK") {
|
||||||
// await ensureDir(TMP_DIR);
|
// throw new Error("Invalid ZIP signature (PK not found)");
|
||||||
|
// }
|
||||||
|
|
||||||
// const zip = new AdmZip(buffer);
|
// // 2. Extract zip ke folder tmp
|
||||||
// zip.extractAllTo(TMP_DIR, true);
|
// const extractDir = path.join(process.cwd(), "tmp_assets");
|
||||||
|
// await fs.rm(extractDir, { recursive: true, force: true });
|
||||||
|
// await fs.mkdir(extractDir, { recursive: true });
|
||||||
|
|
||||||
// /* ===== Process Files ===== */
|
// let zip: AdmZip;
|
||||||
// const files = await walkDir(TMP_DIR);
|
|
||||||
|
|
||||||
|
// 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) {
|
// for (const filePath of files) {
|
||||||
// const entryName = path.basename(filePath);
|
// const entryName = path.basename(filePath);
|
||||||
// const category = detectCategory(entryName);
|
// const category = detectCategory(entryName);
|
||||||
|
|
||||||
// let result;
|
// let finalName = entryName;
|
||||||
|
// let mimeType = "application/octet-stream";
|
||||||
|
// let targetPath = "";
|
||||||
|
|
||||||
// if (category === "image") {
|
// 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 {
|
// } 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({
|
// const existing = await prisma.fileStorage.findUnique({
|
||||||
// where: { name: finalName },
|
// 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) {
|
// if (existing) {
|
||||||
|
// // Restore kalau soft deleted
|
||||||
// await prisma.fileStorage.update({
|
// await prisma.fileStorage.update({
|
||||||
// where: { name: finalName },
|
// where: { name: finalName },
|
||||||
// data,
|
// data: {
|
||||||
|
// path: targetPath,
|
||||||
|
// realName: entryName,
|
||||||
|
// mimeType,
|
||||||
|
// link: `/uploads/${category}/${finalName}`,
|
||||||
|
// category,
|
||||||
|
// deletedAt: null,
|
||||||
|
// isActive: true,
|
||||||
|
// },
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// console.log(`♻️ restored: ${category}/${finalName}`);
|
// console.log(`♻️ restored: ${category}/${finalName}`);
|
||||||
// } else {
|
// } 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(`📂 created: ${category}/${finalName}`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// console.log(`📂 saved: ${category}/${finalName}`);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// /* ===== Cleanup ===== */
|
// // 6. Cleanup
|
||||||
// await fs.rm(TMP_DIR, { recursive: true, force: true });
|
// await fs.rm(extractDir, { recursive: true, force: true });
|
||||||
|
|
||||||
// console.log("✅ Selesai seed assets!");
|
// 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) {
|
// if (import.meta.main) {
|
||||||
// seedAssets()
|
// seedAssets()
|
||||||
// .catch((err) => {
|
// .catch((err) => {
|
||||||
@@ -363,3 +174,382 @@ if (import.meta.main) {
|
|||||||
// await prisma.$disconnect();
|
// 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<FileCategory, string> = {
|
||||||
|
// // 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<string[]> {
|
||||||
|
// // 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<string[]> {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -15,12 +17,10 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
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 { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
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 {
|
function convertToEmbedUrl(youtubeUrl: string): string {
|
||||||
@@ -95,7 +95,9 @@ export default function DetailVideoUser() {
|
|||||||
<Box py="xl" px={{ base: 'md', md: 100 }}>
|
<Box py="xl" px={{ base: 'md', md: 100 }}>
|
||||||
{/* Tombol Kembali */}
|
{/* Tombol Kembali */}
|
||||||
<Box>
|
<Box>
|
||||||
<BackButton />
|
<ActionIcon bg={colors["blue-button"]} onClick={() => router.push('/darmasaba/desa/galery/video')}>
|
||||||
|
<IconArrowLeft />
|
||||||
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Header - Dijadikan Title */}
|
{/* Header - Dijadikan Title */}
|
||||||
|
|||||||
Reference in New Issue
Block a user