CORS UPloads
Deploy / deploy (push) Successful in 3m5s

This commit is contained in:
twotalesanimation
2026-07-22 00:03:02 +02:00
parent 519fe2ad33
commit 15fda1ec44
4 changed files with 194 additions and 60 deletions
+47 -28
View File
@@ -155,36 +155,55 @@ export function BatchUploadClient({ projects }: BatchUploadClientProps) {
try {
if (item.status === "update-highres") {
// MOV high-res files: use XHR so the browser streams the file from
// disk chunk-by-chunk, matching the behaviour of HighResUploadDialog
// which is known to work reliably for large files.
await new Promise<void>((resolve, reject) => {
const fd = new FormData();
fd.append("file", file);
fd.append("action", "update-highres");
fd.append("shotId", item.shotId!);
fd.append("projectId", projectId);
// MOV high-res files: upload directly from the browser to Hetzner
// via a presigned PUT URL so the file never passes through Nginx,
// avoiding HTTP 413 (Request Entity Too Large) errors.
//
// Prerequisite: the Hetzner bucket must have a CORS policy that
// allows PUT from this origin. Add it once via the Hetzner console
// or AWS CLI: aws s3api put-bucket-cors --bucket <bucket> \
// --cors-configuration '{"CORSRules":[{"AllowedOrigins":["*"],
// "AllowedMethods":["PUT"],"AllowedHeaders":["*"]}]}' \
// --endpoint-url <hetzner_endpoint>
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/batch-upload/upload");
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
try {
const json = JSON.parse(xhr.responseText) as { error?: string };
reject(new Error(json.error ?? `HTTP ${xhr.status}`));
} catch {
reject(new Error(`HTTP ${xhr.status}`));
}
}
});
xhr.addEventListener("error", () => reject(new Error("Network error")));
xhr.addEventListener("abort", () => reject(new Error("Upload cancelled")));
xhr.send(fd);
// Step 1 — obtain a presigned PUT URL + server-generated storage key
const presignRes = await fetch("/api/batch-upload/presign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fileName: file.name }),
});
if (!presignRes.ok) {
const d = await presignRes.json().catch(() => ({ error: "Failed to get upload URL" }));
throw new Error((d as { error?: string }).error ?? "Failed to get upload URL");
}
const { presignedUrl, key } = await presignRes.json() as { presignedUrl: string; key: string };
// Step 2 — PUT the file directly to Hetzner (no proxy in the path)
const putRes = await fetch(presignedUrl, {
method: "PUT",
body: file,
headers: { "Content-Type": file.type || "video/quicktime" },
});
if (!putRes.ok) {
throw new Error(
putRes.status === 403
? "Storage upload failed — check Hetzner CORS / bucket policy"
: `Storage upload failed (HTTP ${putRes.status})`
);
}
// Step 3 — commit: record the uploaded key in the DB
const fd = new FormData();
fd.append("action", "update-highres");
fd.append("shotId", item.shotId!);
fd.append("projectId", projectId);
fd.append("key", key);
fd.append("fileName", file.name);
const commitRes = await fetch("/api/batch-upload/upload", { method: "POST", body: fd });
if (!commitRes.ok) {
const d = await commitRes.json().catch(() => ({ error: "Failed to save" }));
throw new Error((d as { error?: string }).error ?? "Failed to save");
}
} else {
// MP4 version uploads: existing fetch-based flow
const fd = new FormData();