import { randomUUID } from "crypto"; import { mkdir, readdir, rm, stat, writeFile } from "fs/promises"; import path from "path"; import { uploadDirectory } from "@/lib/runtime/config"; const maxUploadSize = 5 * 1024 * 1024; const allowedMimeTypes = new Map([ ["image/jpeg", [".jpg", ".jpeg"]], ["image/png", [".png"]], ["image/webp", [".webp"]], ["application/pdf", [".pdf"]], ]); export type MediaFile = { name: string; url: string; type: "image" | "pdf"; extension: string; size: number; uploadedAt: string; isImage: boolean; isPdf: boolean; }; export async function ensureUploadDirectory() { await mkdir(uploadDirectory, { recursive: true }); const probe = path.join(uploadDirectory, `.write-check-${Date.now()}`); await writeFile(probe, "ok", "utf8"); await rm(probe, { force: true }); } export function validateUpload(file: File) { const extensions = allowedMimeTypes.get(file.type); const extension = path.extname(file.name).toLowerCase(); if (!extensions || !extensions.includes(extension)) { return "Bitte eine JPG-, PNG-, WebP- oder PDF-Datei hochladen."; } if (file.size <= 0) { return "Die Datei ist leer."; } if (file.size > maxUploadSize) { return "Die Datei darf maximal 5 MB groß sein."; } return ""; } export function createSafeUploadName(originalName: string) { const extension = path.extname(originalName).toLowerCase(); const rawBaseName = path.basename(originalName, extension); const safeBaseName = rawBaseName .toLowerCase() .normalize("NFKD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 80) || "upload"; return `${Date.now()}-${randomUUID().slice(0, 8)}-${safeBaseName}${extension}`; } export function resolveUploadTarget(fileName: string) { const target = path.resolve(uploadDirectory, fileName); const uploadRoot = path.resolve(uploadDirectory); if (!target.startsWith(`${uploadRoot}${path.sep}`)) { throw new Error("Ungültiger Upload-Pfad."); } return target; } function isAllowedStoredFile(fileName: string) { const extension = path.extname(fileName).toLowerCase(); const allowedExtensions = [...allowedMimeTypes.values()].flat(); return !fileName.startsWith(".") && allowedExtensions.includes(extension); } export async function listMediaFiles(): Promise { await mkdir(uploadDirectory, { recursive: true }); const entries = await readdir(uploadDirectory); const visibleEntries = entries.filter(isAllowedStoredFile); const files = await Promise.all(visibleEntries.map(async (entry) => { const fileStat = await stat(path.join(uploadDirectory, entry)); const extension = path.extname(entry).toLowerCase(); const isImage = [".jpg", ".jpeg", ".png", ".webp"].includes(extension); const isPdf = extension === ".pdf"; return { name: entry, url: `/uploads/images/${encodeURIComponent(entry)}`, type: isPdf ? "pdf" as const : "image" as const, extension: extension.replace(".", "").toUpperCase() || "Datei", size: fileStat.size, uploadedAt: fileStat.birthtime.toISOString(), isImage, isPdf, }; })); return files.sort((left, right) => right.uploadedAt.localeCompare(left.uploadedAt)); } export async function saveMediaFile(file: File) { const error = validateUpload(file); if (error) return { error }; await ensureUploadDirectory(); const safeName = createSafeUploadName(file.name); const target = resolveUploadTarget(safeName); await writeFile(target, Buffer.from(await file.arrayBuffer())); return { fileName: safeName }; } export async function deleteMediaFile(fileName: string) { if (!isAllowedStoredFile(fileName) || path.basename(fileName) !== fileName) { return { error: "Ungültiger Dateiname." }; } await rm(resolveUploadTarget(fileName), { force: true }); return {}; }