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

This commit is contained in:
twotalesanimation
2026-07-29 01:33:40 +02:00
parent e3bdd59087
commit 4c8b57391d
2 changed files with 29 additions and 4 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { generateHetznerPresignedUploadUrl } from "@/lib/storage";
import { generateHetznerPresignedUploadUrl, sanitizeFileName } from "@/lib/storage";
import { randomUUID } from "crypto";
export const maxDuration = 10;
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
ext === "mp4" ? "video/mp4" :
"application/octet-stream";
const key = `highres/${randomUUID()}-${fileName}`;
const key = `highres/${randomUUID()}-${sanitizeFileName(fileName)}`;
try {
const presignedUrl = await generateHetznerPresignedUploadUrl(key, contentType);
+27 -2
View File
@@ -90,6 +90,31 @@ function getBucketName(): string {
return map[provider] ?? "vfx-review";
}
/**
* Sanitize a user-supplied filename before it becomes (part of) an S3 object key.
*
* Reserved/special URL characters in a key — especially `#` (fragment) and
* `?`/`%` (query / percent-encoding) — break S3 presigned-URL signatures:
* the signature is computed over the raw key, but browsers/HTTP clients
* strip or re-encode those characters differently when the URL is actually
* fetched, producing a signature mismatch. That surfaces to users as a 400
* when the image is later loaded. Common real-world filenames like
* "Take #3.png" or "Ref #12 (final).jpg" are common enough in VFX pipelines
* to hit this regularly, so every key built from a filename must go through
* this sanitizer.
*/
export function sanitizeFileName(fileName: string): string {
const ext = path.extname(fileName);
const base = path.basename(fileName, ext);
const safeBase = base
.normalize("NFKD")
.replace(/[^\w.-]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "");
const safeExt = ext.replace(/[^\w.-]+/g, "");
return `${safeBase || "file"}${safeExt}`;
}
// ── Public API ───────────────────────────────────────────────────────────────
export interface UploadResult {
@@ -109,7 +134,7 @@ export async function uploadFile(
folder: string = "uploads"
): Promise<UploadResult> {
const provider = getProvider();
const key = `${folder}/${randomUUID()}-${fileName}`;
const key = `${folder}/${randomUUID()}-${sanitizeFileName(fileName)}`;
if (provider === "local") {
return uploadLocal(buffer, key);
@@ -266,7 +291,7 @@ export async function uploadToHetzner(
contentType: string,
folder: string = "highres"
): Promise<{ key: string }> {
const key = `${folder}/${randomUUID()}-${fileName}`;
const key = `${folder}/${randomUUID()}-${sanitizeFileName(fileName)}`;
const { client, bucket } = await buildHetznerClient();
await client.send(
new PutObjectCommand({