Files
vfxreview/app/api/admin/migration/route.ts
T
twotalesanimation 745634f1b6
Deploy / deploy (push) Successful in 2m43s
BIG MOOOOVE to Object Storage
2026-07-16 22:15:58 +02:00

117 lines
3.4 KiB
TypeScript

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<string, string> = {
".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<string>();
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 }
);
}
}