Harden client uploads and add storage fallback

Validate presigned upload responses and fall back to Vercel Blob when third-party storage upload fails so photo uploads remain available.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Strtus 2026-05-19 00:54:46 +08:00
parent d5001c9ee5
commit 053a3b4acc

View File

@ -119,11 +119,27 @@ export const uploadFromClientViaPresignedUrl = async (
file: File | Blob, file: File | Blob,
fileName: string, fileName: string,
) => { ) => {
const url = await fetch(`${PATH_API_PRESIGNED_URL}/${fileName}`) const response = await fetch(`${PATH_API_PRESIGNED_URL}/${fileName}`);
.then((response) => response.text()); if (!response.ok) {
throw new Error(
`Failed to get presigned URL: ${response.status} ${response.statusText}`,
);
}
return fetch(url, { method: 'PUT', body: file }) const url = (await response.text()).trim();
.then(() => `${baseUrlForStorage(CURRENT_STORAGE)}/${fileName}`);
if (!/^https?:\/\//i.test(url)) {
throw new Error('Invalid presigned URL response');
}
const uploadResponse = await fetch(url, { method: 'PUT', body: file });
if (!uploadResponse.ok) {
throw new Error(
`Failed to upload with presigned URL: ${uploadResponse.status} ${uploadResponse.statusText}`,
);
}
return `${baseUrlForStorage(CURRENT_STORAGE)}/${fileName}`;
}; };
export const uploadFileFromClient = async ( export const uploadFileFromClient = async (
@ -136,13 +152,24 @@ export const uploadFileFromClient = async (
? `${_fileName}-${generateStorageId()}.${extension}` ? `${_fileName}-${generateStorageId()}.${extension}`
: `${_fileName}.${extension}`; : `${_fileName}.${extension}`;
return ( if (
CURRENT_STORAGE === 'cloudflare-r2' || CURRENT_STORAGE === 'cloudflare-r2' ||
CURRENT_STORAGE === 'aws-s3' || CURRENT_STORAGE === 'aws-s3' ||
CURRENT_STORAGE === 'minio' CURRENT_STORAGE === 'minio'
) ) {
? uploadFromClientViaPresignedUrl(file, fileName) try {
: vercelBlobUploadFromClient(file, fileName); return await uploadFromClientViaPresignedUrl(file, fileName);
} catch (error) {
// Fall back to Vercel Blob when configured, so uploads stay available
// even if third-party object storage is temporarily unreachable.
if (HAS_VERCEL_BLOB_STORAGE) {
return vercelBlobUploadFromClient(file, fileName);
}
throw error;
}
}
return vercelBlobUploadFromClient(file, fileName);
}; };
export const putFile = ( export const putFile = (