import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/auth"; import fs from "fs"; import path from "path"; import { listHetznerKeys, uploadToHetznerWithKey } from "@/lib/storage"; export const maxDuration = 300; // 5 min — large video uploads const MIME_MAP: Record = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".avi": "video/x-msvideo", ".mxf": "application/mxf", ".webm": "video/webm", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp", }; function walkDir( dir: string, base: string ): Array<{ key: string; sizeBytes: number }> { const results: Array<{ key: string; sizeBytes: number }> = []; if (!fs.existsSync(dir)) return results; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const abs = path.join(dir, entry.name); if (entry.isDirectory()) { results.push(...walkDir(abs, base)); } else if (entry.isFile()) { const key = path.relative(base, abs).replace(/\\/g, "/"); results.push({ key, sizeBytes: fs.statSync(abs).size }); } } return results; } /** GET /api/admin/migration — list local files with Hetzner sync status */ export async function GET() { const session = await auth(); if (!session?.user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const uploadDir = path.resolve(process.env.LOCAL_UPLOAD_DIR ?? "./uploads"); const localFiles = walkDir(uploadDir, uploadDir); let hetznerKeys = new Set(); let hetznerConfigured = true; try { hetznerKeys = await listHetznerKeys(); } catch { hetznerConfigured = false; } const files = localFiles.map(({ key, sizeBytes }) => ({ key, filename: path.basename(key), folder: path.dirname(key).replace(/\\/g, "/") || "root", sizeBytes, hetznerExists: hetznerKeys.has(key), })); return NextResponse.json({ files, hetznerConfigured }); } /** POST /api/admin/migration — migrate a single file to Hetzner */ export async function POST(req: NextRequest) { const session = await auth(); if (!session?.user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json(); const key = body?.key; if (!key || typeof key !== "string") { return NextResponse.json({ error: "key is required" }, { status: 400 }); } const uploadDir = path.resolve(process.env.LOCAL_UPLOAD_DIR ?? "./uploads"); const absPath = path.resolve(path.join(uploadDir, key)); // Path traversal guard if (!absPath.startsWith(uploadDir + path.sep)) { return NextResponse.json({ error: "Invalid key" }, { status: 403 }); } let stat: fs.Stats; try { stat = fs.statSync(absPath); } catch { return NextResponse.json({ error: "File not found on disk" }, { status: 404 }); } if (!stat.isFile()) { return NextResponse.json({ error: "Not a file" }, { status: 400 }); } const ext = path.extname(key).toLowerCase(); const contentType = MIME_MAP[ext] ?? "application/octet-stream"; const start = Date.now(); try { const stream = fs.createReadStream(absPath); await uploadToHetznerWithKey(stream, key, contentType, stat.size); return NextResponse.json({ success: true, key, ms: Date.now() - start }); } catch (e: unknown) { return NextResponse.json( { error: e instanceof Error ? e.message : "Upload failed", key }, { status: 500 } ); } }