@@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { generateHetznerPresignedUploadUrl } from "@/lib/storage";
|
import { generateHetznerPresignedUploadUrl, sanitizeFileName } from "@/lib/storage";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
export const maxDuration = 10;
|
export const maxDuration = 10;
|
||||||
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
|
|||||||
ext === "mp4" ? "video/mp4" :
|
ext === "mp4" ? "video/mp4" :
|
||||||
"application/octet-stream";
|
"application/octet-stream";
|
||||||
|
|
||||||
const key = `highres/${randomUUID()}-${fileName}`;
|
const key = `highres/${randomUUID()}-${sanitizeFileName(fileName)}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const presignedUrl = await generateHetznerPresignedUploadUrl(key, contentType);
|
const presignedUrl = await generateHetznerPresignedUploadUrl(key, contentType);
|
||||||
|
|||||||
+27
-2
@@ -90,6 +90,31 @@ function getBucketName(): string {
|
|||||||
return map[provider] ?? "vfx-review";
|
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 ───────────────────────────────────────────────────────────────
|
// ── Public API ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface UploadResult {
|
export interface UploadResult {
|
||||||
@@ -109,7 +134,7 @@ export async function uploadFile(
|
|||||||
folder: string = "uploads"
|
folder: string = "uploads"
|
||||||
): Promise<UploadResult> {
|
): Promise<UploadResult> {
|
||||||
const provider = getProvider();
|
const provider = getProvider();
|
||||||
const key = `${folder}/${randomUUID()}-${fileName}`;
|
const key = `${folder}/${randomUUID()}-${sanitizeFileName(fileName)}`;
|
||||||
|
|
||||||
if (provider === "local") {
|
if (provider === "local") {
|
||||||
return uploadLocal(buffer, key);
|
return uploadLocal(buffer, key);
|
||||||
@@ -266,7 +291,7 @@ export async function uploadToHetzner(
|
|||||||
contentType: string,
|
contentType: string,
|
||||||
folder: string = "highres"
|
folder: string = "highres"
|
||||||
): Promise<{ key: string }> {
|
): Promise<{ key: string }> {
|
||||||
const key = `${folder}/${randomUUID()}-${fileName}`;
|
const key = `${folder}/${randomUUID()}-${sanitizeFileName(fileName)}`;
|
||||||
const { client, bucket } = await buildHetznerClient();
|
const { client, bucket } = await buildHetznerClient();
|
||||||
await client.send(
|
await client.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
|
|||||||
Reference in New Issue
Block a user