mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(audio-upload): implement audio file size validation and error handling
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '@/lib/guest-upload-token';
|
} from '@/lib/guest-upload-token';
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
const MAX_MULTIPART_BODY_SIZE = MAX_FILE_SIZE + (512 * 1024); // file + multipart overhead
|
||||||
|
|
||||||
// Canonical MIME types accepted
|
// Canonical MIME types accepted
|
||||||
const ALLOWED_TYPES = new Set(['audio/webm', 'audio/ogg', 'audio/opus', 'audio/mp4', 'audio/mpeg', 'audio/wav']);
|
const ALLOWED_TYPES = new Set(['audio/webm', 'audio/ogg', 'audio/opus', 'audio/mp4', 'audio/mpeg', 'audio/wav']);
|
||||||
@@ -89,8 +90,8 @@ export async function POST(request: NextRequest) {
|
|||||||
// Check Content-Length header BEFORE loading the file
|
// Check Content-Length header BEFORE loading the file
|
||||||
const contentLength = request.headers.get('content-length');
|
const contentLength = request.headers.get('content-length');
|
||||||
if (contentLength) {
|
if (contentLength) {
|
||||||
const fileSize = parseInt(contentLength, 10);
|
const bodySize = parseInt(contentLength, 10);
|
||||||
if (isNaN(fileSize) || fileSize > MAX_FILE_SIZE) {
|
if (isNaN(bodySize) || bodySize > MAX_MULTIPART_BODY_SIZE) {
|
||||||
return apiErrors.badRequest('File too large. Maximum size is 10MB.');
|
return apiErrors.badRequest('File too large. Maximum size is 10MB.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,39 @@ import { useCommentMedia } from '@/components/video-page/hooks/use-comment-media
|
|||||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const MAX_AUDIO_UPLOAD_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
const MAX_AUDIO_UPLOAD_SIZE_MESSAGE = 'File too large. Maximum size is 10MB.';
|
||||||
|
|
||||||
function formatTime(seconds: number): string {
|
function formatTime(seconds: number): string {
|
||||||
const s = Math.floor(seconds);
|
const s = Math.floor(seconds);
|
||||||
return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
|
return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UploadAudioResponse = {
|
||||||
|
data?: { url?: string };
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAudioUploadValidationError(file: Blob): string | null {
|
||||||
|
if (file.size > MAX_AUDIO_UPLOAD_SIZE) {
|
||||||
|
return MAX_AUDIO_UPLOAD_SIZE_MESSAGE;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readUploadAudioResponse(response: Response): Promise<UploadAudioResponse | null> {
|
||||||
|
const raw = await response.text().catch(() => '');
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed) as UploadAudioResponse;
|
||||||
|
} catch {
|
||||||
|
if (trimmed.startsWith('<')) return null;
|
||||||
|
return { error: trimmed };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface AssetsPaneProps {
|
interface AssetsPaneProps {
|
||||||
videoId: string;
|
videoId: string;
|
||||||
assets: VideoAsset[];
|
assets: VideoAsset[];
|
||||||
@@ -513,6 +541,13 @@ export const AssetsPane = memo(function AssetsPane({
|
|||||||
const handleVoiceUpload = useCallback(async () => {
|
const handleVoiceUpload = useCallback(async () => {
|
||||||
const uploadSource = pendingAudioFile ?? audioBlob;
|
const uploadSource = pendingAudioFile ?? audioBlob;
|
||||||
if (!uploadSource) return;
|
if (!uploadSource) return;
|
||||||
|
|
||||||
|
const validationError = getAudioUploadValidationError(uploadSource);
|
||||||
|
if (validationError) {
|
||||||
|
toast.error(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsUploadingVoice(true);
|
setIsUploadingVoice(true);
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -529,10 +564,14 @@ export const AssetsPane = memo(function AssetsPane({
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
const uploadPayload = (await uploadRes.json().catch(() => null)) as { data?: { url?: string }; error?: string } | null;
|
const uploadPayload = await readUploadAudioResponse(uploadRes);
|
||||||
const uploadedUrl = uploadPayload?.data?.url;
|
const uploadedUrl = uploadPayload?.data?.url;
|
||||||
if (!uploadRes.ok || !uploadedUrl) {
|
if (!uploadRes.ok || !uploadedUrl) {
|
||||||
toast.error(uploadPayload?.error || 'Failed to upload voice recording');
|
toast.error(
|
||||||
|
uploadPayload?.error
|
||||||
|
|| (uploadRes.status === 413 ? MAX_AUDIO_UPLOAD_SIZE_MESSAGE : null)
|
||||||
|
|| 'Failed to upload voice recording'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,8 +587,9 @@ export const AssetsPane = memo(function AssetsPane({
|
|||||||
setAudioBlob(null);
|
setAudioBlob(null);
|
||||||
setAudioBlobUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return null; });
|
setAudioBlobUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return null; });
|
||||||
setPendingAudioFile(null);
|
setPendingAudioFile(null);
|
||||||
} catch {
|
} catch (error) {
|
||||||
toast.error('Failed to upload voice recording');
|
console.error('Failed to upload voice asset:', error);
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Failed to upload voice recording');
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingVoice(false);
|
setIsUploadingVoice(false);
|
||||||
}
|
}
|
||||||
@@ -592,6 +632,11 @@ export const AssetsPane = memo(function AssetsPane({
|
|||||||
setUploadTab('bunny');
|
setUploadTab('bunny');
|
||||||
await handleBunnyFileUpload(file);
|
await handleBunnyFileUpload(file);
|
||||||
} else if (file.type.startsWith('audio/')) {
|
} else if (file.type.startsWith('audio/')) {
|
||||||
|
const audioError = getAudioUploadValidationError(file);
|
||||||
|
if (audioError) {
|
||||||
|
toast.error(audioError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Stage the file so the user can optionally set a name before uploading
|
// Stage the file so the user can optionally set a name before uploading
|
||||||
setUploadTab('voice');
|
setUploadTab('voice');
|
||||||
setPendingAudioFile(file);
|
setPendingAudioFile(file);
|
||||||
|
|||||||
Reference in New Issue
Block a user