Image url update
Deploy / deploy (push) Successful in 2m30s

This commit is contained in:
twotalesanimation
2026-07-29 07:46:00 +02:00
parent f0dcf17e93
commit 0d0f3e1a33
3 changed files with 142 additions and 54 deletions
+69
View File
@@ -0,0 +1,69 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { randomUUID } from "crypto";
import { generateHetznerPresignedUploadUrl, sanitizeFileName } from "@/lib/storage";
export const maxDuration = 10;
const ALLOWED_FOLDERS = new Set(["videos", "image"]);
/**
* POST /api/upload/presign
* Body: { fileName: string, contentType: string, folder?: "videos" | "image" }
*
* Returns a presigned PUT URL so the browser can upload large files (video
* versions, footage plates) directly to Hetzner Object Storage, bypassing
* this app's server entirely — and with it any reverse-proxy / CDN body
* size limits (nginx `client_max_body_size`, Cloudflare's 100MB edge cap,
* etc). The returned `key`/`url` is passed straight to the relevant server
* action (createVersion, addFootagePlate, ...) once the direct upload
* finishes — no bytes ever touch the Next.js server.
*/
export async function POST(req: NextRequest) {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}
const { fileName, contentType, folder } = body as {
fileName?: unknown;
contentType?: unknown;
folder?: unknown;
};
if (!fileName || typeof fileName !== "string") {
return NextResponse.json({ error: "fileName is required" }, { status: 400 });
}
if (!contentType || typeof contentType !== "string") {
return NextResponse.json({ error: "contentType is required" }, { status: 400 });
}
const targetFolder = typeof folder === "string" && ALLOWED_FOLDERS.has(folder) ? folder : "videos";
if (targetFolder === "videos" && !contentType.match(/^video\//)) {
return NextResponse.json({ error: "Only video files are accepted" }, { status: 400 });
}
if (targetFolder === "image" && !contentType.match(/^image\//)) {
return NextResponse.json({ error: "Only image files are accepted" }, { status: 400 });
}
const key = `${targetFolder}/${randomUUID()}-${sanitizeFileName(fileName)}`;
try {
const presignedUrl = await generateHetznerPresignedUploadUrl(key, contentType);
return NextResponse.json({ presignedUrl, key, url: `/api/files/${key}` });
} catch (err) {
console.error("[upload/presign]", err);
return NextResponse.json(
{ error: "Failed to generate upload URL. Check Hetzner storage configuration." },
{ status: 500 }
);
}
}