+27
-2
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user