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

185 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = 120;
/**
* 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<NextResponse> {
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());
// ── 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, file.type, "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, file.type, "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,
});
}