import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { uploadToHetzner, deleteFromHetzner } from "@/lib/storage"; import { recalcShotStatus } from "@/lib/shot-status"; export const maxDuration = 300; // 5 min — large .mov high-res files /** * POST /api/batch-upload/upload * * Handles a single file upload from the batch upload flow. * * FormData fields: * file – the binary file * action – "new-version" | "rename-and-upload" | "create-task" | "update-highres" * shotId – shot DB id (required for all actions) * projectId – project DB id * taskId – required for "new-version" and "rename-and-upload" * fallbackTaskId – optional: the "Comp" task id to rename for "create-task" * newTaskTitle – desired task title (filename without extension) */ export async function POST( req: NextRequest ): Promise { const session = await auth(); if (!session?.user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const formData = await req.formData(); const file = formData.get("file") as File | null; const action = formData.get("action") as string | null; const shotId = formData.get("shotId") as string | null; const projectId = formData.get("projectId") as string | null; const taskId = formData.get("taskId") as string | null; const fallbackTaskId = formData.get("fallbackTaskId") as string | null; const newTaskTitle = (formData.get("newTaskTitle") as string | null) ?? ""; if (!file || !action || !shotId || !projectId) { return NextResponse.json( { error: "Missing required fields: file, action, shotId, projectId" }, { status: 400 } ); } const buffer = Buffer.from(await file.arrayBuffer()); // Browsers (especially on Windows) often send an empty MIME type for .mov // files. Fall back to a safe content type based on the file extension. const ext = file.name.split(".").pop()?.toLowerCase(); const contentType = file.type || (ext === "mov" ? "video/quicktime" : ext === "mp4" ? "video/mp4" : "application/octet-stream"); // ── High-res (.mov) ─────────────────────────────────────────────────────── if (action === "update-highres") { const shot = await db.shot.findUnique({ where: { id: shotId }, select: { id: true, highResKey: true }, }); if (!shot) { return NextResponse.json({ error: "Shot not found" }, { status: 404 }); } // Remove the previous high-res file if one exists if (shot.highResKey) { await deleteFromHetzner(shot.highResKey).catch(() => {}); } const { key } = await uploadToHetzner(buffer, file.name, contentType, "highres"); await db.shot.update({ where: { id: shotId }, data: { highResKey: key, highResFilename: file.name }, }); return NextResponse.json({ success: true, action: "update-highres", fileName: file.name, }); } // ── Version upload (.mp4) ───────────────────────────────────────────────── let resolvedTaskId: string; if (action === "new-version") { if (!taskId) { return NextResponse.json( { error: "taskId is required for new-version action" }, { status: 400 } ); } resolvedTaskId = taskId; } else if (action === "rename-and-upload") { if (!taskId) { return NextResponse.json( { error: "taskId is required for rename-and-upload action" }, { status: 400 } ); } // Rename the task to reflect the new version number await db.task.update({ where: { id: taskId }, data: { title: newTaskTitle }, }); resolvedTaskId = taskId; } else if (action === "create-task") { if (fallbackTaskId) { // Rename the existing "Comp" (or similar) task and reuse it await db.task.update({ where: { id: fallbackTaskId }, data: { title: newTaskTitle }, }); resolvedTaskId = fallbackTaskId; } else { // Create a brand-new task under the shot const lastTask = await db.task.findFirst({ where: { shotId }, orderBy: { sortOrder: "desc" }, select: { sortOrder: true }, }); const task = await db.task.create({ data: { title: newTaskTitle, type: "COMP", shotId, projectId, createdById: session.user.id, sortOrder: (lastTask?.sortOrder ?? -1) + 1, }, }); resolvedTaskId = task.id; } } else { return NextResponse.json( { error: `Unknown action: ${action}` }, { status: 400 } ); } // Upload the video to Hetzner object storage const { key: videoKey } = await uploadToHetzner(buffer, file.name, contentType, "videos"); const result = { url: `/api/files/${videoKey}`, key: videoKey }; // Mark all existing versions for this task as no longer latest await db.version.updateMany({ where: { taskId: resolvedTaskId }, data: { isLatest: false }, }); // Determine next sequential version number const lastVersion = await db.version.findFirst({ where: { taskId: resolvedTaskId }, orderBy: { versionNumber: "desc" }, select: { versionNumber: true }, }); const versionNumber = (lastVersion?.versionNumber ?? 0) + 1; // Create the version record const version = await db.version.create({ data: { versionNumber, taskId: resolvedTaskId, artistId: session.user.id, fileUrl: result.url, fileName: file.name, fileSize: BigInt(file.size), mimeType: file.type, isLatest: true, }, }); // Move the task to INTERNAL_REVIEW await db.task.update({ where: { id: resolvedTaskId }, data: { status: "INTERNAL_REVIEW" }, }); // Recalculate the parent shot's status await recalcShotStatus(shotId).catch(() => {}); return NextResponse.json({ success: true, action, versionId: version.id, versionNumber, taskId: resolvedTaskId, }); }