2dee1bf6fc
Deploy / deploy (push) Successful in 6m39s
Adds a third section to the Batch Upload page that attaches plate movies to shots derived from the filename: DH2_VFX_058_120_REF.mov becomes the plate "REF" on shot DH2_VFX_058_120. Shot matching reuses findMatchingShotCode, so it behaves like the rest of the page (case-insensitive, longest code wins, underscore-boundary only) and the filename remainder becomes the label. Bytes go straight to Hetzner via the existing presign endpoint since plates are multi-GB, with XHR for per-file progress. Files whose shot already has a plate of the same name are skipped so re-running a batch does not stack duplicates. Also resets the thumbnail and plate previews when the project changes — they are resolved against a specific project, but only the version-upload flow was being cleared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
2.7 KiB
TypeScript
97 lines
2.7 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import { revalidatePath } from "next/cache";
|
|
|
|
export const maxDuration = 30;
|
|
|
|
/**
|
|
* POST /api/batch-upload/plates/upload
|
|
* Body: { projectId, shotId, key, fileName, fileSize?, label? }
|
|
*
|
|
* Commits a footage plate whose bytes were already uploaded straight to
|
|
* Hetzner via POST /api/upload/presign, so large .mov plates never pass
|
|
* through this server or the reverse proxy.
|
|
*/
|
|
export async function POST(req: NextRequest) {
|
|
const session = await auth();
|
|
if (!session?.user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
if (!["ADMIN", "PRODUCER", "SUPERVISOR"].includes(session.user.role)) {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
|
}
|
|
|
|
const { projectId, shotId, key, fileName, fileSize, label } = body as {
|
|
projectId?: unknown;
|
|
shotId?: unknown;
|
|
key?: unknown;
|
|
fileName?: unknown;
|
|
fileSize?: unknown;
|
|
label?: unknown;
|
|
};
|
|
|
|
if (
|
|
typeof projectId !== "string" ||
|
|
typeof shotId !== "string" ||
|
|
typeof key !== "string" ||
|
|
typeof fileName !== "string" ||
|
|
!projectId ||
|
|
!shotId ||
|
|
!key ||
|
|
!fileName
|
|
) {
|
|
return NextResponse.json(
|
|
{ error: "projectId, shotId, key and fileName are required" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// The key must be one this app handed out via /api/upload/presign.
|
|
if (!key.startsWith("videos/")) {
|
|
return NextResponse.json({ error: "Invalid storage key" }, { status: 400 });
|
|
}
|
|
|
|
const shot = await db.shot.findFirst({
|
|
where: { id: shotId, projectId },
|
|
select: { id: true },
|
|
});
|
|
if (!shot) return NextResponse.json({ error: "Shot not found" }, { status: 404 });
|
|
|
|
const last = await db.footagePlate.findFirst({
|
|
where: { shotId },
|
|
orderBy: { sortOrder: "desc" },
|
|
select: { sortOrder: true },
|
|
});
|
|
|
|
const size = typeof fileSize === "number" && Number.isFinite(fileSize) && fileSize > 0
|
|
? BigInt(Math.round(fileSize))
|
|
: null;
|
|
|
|
const plate = await db.footagePlate.create({
|
|
data: {
|
|
shotId,
|
|
fileUrl: `/api/files/${key}`,
|
|
fileKey: key,
|
|
fileName,
|
|
fileSize: size,
|
|
label: typeof label === "string" ? label.trim().slice(0, 100) : "",
|
|
sortOrder: (last?.sortOrder ?? -1) + 1,
|
|
},
|
|
});
|
|
|
|
revalidatePath(`/projects/${projectId}`);
|
|
revalidatePath(`/projects/${projectId}/shots/${shotId}`);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
plateId: plate.id,
|
|
label: plate.label,
|
|
});
|
|
}
|