fix(storage): migrate fileStorage handlers and seeder from local disk/Seafile to MinIO
Root cause: images not showing because:
1. seed_assets.ts was listing from Seafile (unreliable, 502 errors)
2. fileStorage create/findUniq/del handlers used local disk (not available in containers)
3. link format in file-storage.json had inconsistent path prefix
Changes:
- seed_assets.ts: list objects from MinIO bucket instead of Seafile API; seed
link as /api/img/{name}, path as "image" (matches MinIO prefix)
- fileStorage/create.ts: upload image/audio/document to MinIO via putObject;
remove WIBU_UPLOAD_DIR dependency and all fs.writeFile calls
- fileStorage/findUniq.ts: stream file from MinIO via getObject; remove
WIBU_UPLOAD_DIR and fs.readFile
- fileStorage/del.ts: delete from MinIO via removeObject; remove fs.unlink
- file-storage.json: fix path field to "image" (was full path); all 80 entries
already have correct link=/api/img/{name} format
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,41 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { nanoid } from "nanoid";
|
||||
import sharp from "sharp";
|
||||
import zlib from "zlib";
|
||||
|
||||
const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR;
|
||||
import minio, { MINIO_BUCKET } from "@/lib/minio";
|
||||
|
||||
const fileStorageCreate = async (context: Context) => {
|
||||
const body = (await context.body) as {
|
||||
name: string;
|
||||
file: File;
|
||||
};
|
||||
const body = (await context.body) as { name: string; file: File };
|
||||
|
||||
const file = body.file;
|
||||
const name = body.name;
|
||||
|
||||
if (!file) return { status: 400, body: "No file uploaded" };
|
||||
if (!name) return { status: 400, body: "No name provided" };
|
||||
if (!UPLOAD_DIR) return { status: 500, body: "UPLOAD_DIR is not defined" };
|
||||
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isAudio = file.type.startsWith("audio/");
|
||||
const category = isImage ? "image" : isAudio ? "audio" : "document";
|
||||
const prefix = category === "image" ? "image" : category === "audio" ? "audio" : "documents";
|
||||
|
||||
const pathName = category === "image" ? "images" : category === "audio" ? "audio" : "documents";
|
||||
const rootPath = path.join(UPLOAD_DIR, pathName);
|
||||
await fs.mkdir(rootPath, { recursive: true });
|
||||
|
||||
// Convert File ke Buffer
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
let finalName = nanoid();
|
||||
let finalMimeType = file.type;
|
||||
|
||||
if (isImage) {
|
||||
// Simpan sebagai WebP untuk kompresi maksimal
|
||||
const mobileBuffer = await sharp(buffer)
|
||||
.resize({ width: 720 })
|
||||
.webp({ quality: 80 })
|
||||
@@ -49,40 +36,29 @@ const fileStorageCreate = async (context: Context) => {
|
||||
const mobileName = `${finalName}-mobile.webp`;
|
||||
const desktopName = `${finalName}-desktop.webp`;
|
||||
|
||||
await fs.writeFile(path.join(rootPath, mobileName), mobileBuffer);
|
||||
await fs.writeFile(path.join(rootPath, desktopName), desktopBuffer);
|
||||
await minio.putObject(MINIO_BUCKET, `${prefix}/${mobileName}`, mobileBuffer, mobileBuffer.length, { "Content-Type": "image/webp" });
|
||||
await minio.putObject(MINIO_BUCKET, `${prefix}/${desktopName}`, desktopBuffer, desktopBuffer.length, { "Content-Type": "image/webp" });
|
||||
|
||||
// Simpan metadata untuk versi desktop sebagai default
|
||||
finalName = desktopName;
|
||||
finalMimeType = "image/webp";
|
||||
} else if (isAudio) {
|
||||
// Simpan file audio tanpa kompresi
|
||||
const ext = file.name.split(".").pop() || "mp3";
|
||||
finalName = `${finalName}.${ext}`;
|
||||
await fs.writeFile(path.join(rootPath, finalName), buffer);
|
||||
await minio.putObject(MINIO_BUCKET, `${prefix}/${finalName}`, buffer, buffer.length, { "Content-Type": file.type });
|
||||
} else {
|
||||
// Jika file adalah PDF, simpan tanpa kompresi
|
||||
if (file.type === "application/pdf") {
|
||||
await fs.writeFile(path.join(rootPath, finalName), buffer);
|
||||
}
|
||||
// Jika file lain, kompres dengan gzip
|
||||
else {
|
||||
const gzBuffer = zlib.gzipSync(buffer);
|
||||
const compressedName = `${finalName}.gz`;
|
||||
await fs.writeFile(path.join(rootPath, compressedName), gzBuffer);
|
||||
finalName = compressedName;
|
||||
finalMimeType = "application/gzip";
|
||||
}
|
||||
const ext = file.name.split(".").pop() || "bin";
|
||||
finalName = `${finalName}.${ext}`;
|
||||
await minio.putObject(MINIO_BUCKET, `${prefix}/${finalName}`, buffer, buffer.length, { "Content-Type": file.type });
|
||||
}
|
||||
|
||||
const data = await prisma.fileStorage.create({
|
||||
data: {
|
||||
name: finalName,
|
||||
realName: file.name,
|
||||
path: pathName, // Store relative path (e.g., "images", "audio", "documents")
|
||||
path: prefix,
|
||||
mimeType: finalMimeType,
|
||||
category,
|
||||
link: `/api/fileStorage/findUnique/${finalName}`,
|
||||
link: isImage ? `/api/img/${finalName}` : `/api/fileStorage/findUnique/${finalName}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,60 +1,26 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR;
|
||||
import minio, { MINIO_BUCKET } from "@/lib/minio";
|
||||
|
||||
const fileStorageDelete = async (context: Context) => {
|
||||
const { params } = context;
|
||||
const id = (context.params as { id: string })?.id;
|
||||
|
||||
const id = params?.id as string;
|
||||
if (!id) return { status: 400, body: "ID file tidak ditemukan" };
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
status: 400,
|
||||
body: "ID file tidak ditemukan",
|
||||
};
|
||||
}
|
||||
const file = await prisma.fileStorage.findUnique({ where: { id } });
|
||||
|
||||
if (!UPLOAD_DIR) {
|
||||
return {
|
||||
status: 500,
|
||||
body: "UPLOAD_DIR belum dikonfigurasi",
|
||||
};
|
||||
}
|
||||
|
||||
// Cek file dari database
|
||||
const file = await prisma.fileStorage.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
return {
|
||||
status: 404,
|
||||
body: "File tidak ditemukan di database",
|
||||
};
|
||||
}
|
||||
|
||||
const filePath = path.join(UPLOAD_DIR, file.path, file.name);
|
||||
if (!file) return { status: 404, body: "File tidak ditemukan di database" };
|
||||
|
||||
try {
|
||||
// Hapus file dari filesystem
|
||||
await fs.unlink(filePath);
|
||||
await minio.removeObject(MINIO_BUCKET, `${file.path}/${file.name}`);
|
||||
} catch (err) {
|
||||
console.error("Gagal hapus file:", err);
|
||||
// Tetap lanjutkan hapus dari database meskipun file fisik tidak ditemukan
|
||||
console.error("Gagal hapus file dari MinIO:", err);
|
||||
// Tetap lanjutkan hapus dari database
|
||||
}
|
||||
|
||||
// Hapus dari database
|
||||
await prisma.fileStorage.delete({
|
||||
where: { id },
|
||||
});
|
||||
await prisma.fileStorage.delete({ where: { id } });
|
||||
|
||||
return {
|
||||
message: "File berhasil dihapus",
|
||||
deletedId: id,
|
||||
};
|
||||
return { message: "File berhasil dihapus", deletedId: id };
|
||||
};
|
||||
|
||||
export default fileStorageDelete;
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR;
|
||||
import minio, { MINIO_BUCKET } from "@/lib/minio";
|
||||
|
||||
const fileStorageFindUnique = async (context: Context) => {
|
||||
const { name } = context.params;
|
||||
|
||||
const data = await prisma.fileStorage.findUnique({
|
||||
where: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
const data = await prisma.fileStorage.findUnique({ where: { name } });
|
||||
|
||||
if (!data) {
|
||||
context.set.status = "No Content";
|
||||
return {
|
||||
status: 404,
|
||||
message: "File not found",
|
||||
};
|
||||
context.set.status = "Not Found";
|
||||
return { status: 404, message: "File not found" };
|
||||
}
|
||||
|
||||
if (!UPLOAD_DIR) {
|
||||
context.set.status = "Internal Server Error";
|
||||
return {
|
||||
status: 500,
|
||||
message: "UPLOAD_DIR is not defined",
|
||||
try {
|
||||
const stream = await minio.getObject(MINIO_BUCKET, `${data.path}/${data.name}`);
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
const file = Buffer.concat(chunks);
|
||||
|
||||
context.set.headers = {
|
||||
"Content-Type": data.mimeType,
|
||||
"Content-Length": String(file.length),
|
||||
"Cache-Control": "public, max-age=3600, stale-while-revalidate=600",
|
||||
};
|
||||
|
||||
return file;
|
||||
} catch {
|
||||
context.set.status = "Not Found";
|
||||
return { status: 404, message: "File not found in storage" };
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
const file = await fs.readFile(path.join(UPLOAD_DIR, data.path, data.name));
|
||||
context.set.headers = {
|
||||
"Content-Type": data.mimeType,
|
||||
"Content-Length": file.length,
|
||||
};
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
export default fileStorageFindUnique;
|
||||
|
||||
Reference in New Issue
Block a user