From 6bad0d42cadb183c83f615f4787d1b96ba4114f7 Mon Sep 17 00:00:00 2001 From: Abelino Chavez Date: Fri, 30 Jan 2026 20:40:54 -0600 Subject: [PATCH] feat: add file size validation for media uploads (#653) Validates file sizes before processing media uploads: - Image limit: 50MB - Video limit: 500MB - Audio limit: 100MB Shows clear error messages with actual file size when exceeded. Valid files still process even when some files are rejected. Co-Authored-By: Claude Opus 4.5 --- .../editor/panels/assets/views/assets.tsx | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/editor/panels/assets/views/assets.tsx b/apps/web/src/components/editor/panels/assets/views/assets.tsx index 808961f1..55fabd8c 100644 --- a/apps/web/src/components/editor/panels/assets/views/assets.tsx +++ b/apps/web/src/components/editor/panels/assets/views/assets.tsx @@ -45,6 +45,45 @@ import { } from "@hugeicons/core-free-icons"; import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react"; +// File size limits in bytes +const FILE_SIZE_LIMITS = { + image: 50 * 1024 * 1024, // 50MB + video: 500 * 1024 * 1024, // 500MB + audio: 100 * 1024 * 1024, // 100MB +} as const; + +function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 * 1024 * 1024) + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; +} + +function getMediaTypeFromFile(file: File): "image" | "video" | "audio" | null { + if (file.type.startsWith("image/")) return "image"; + if (file.type.startsWith("video/")) return "video"; + if (file.type.startsWith("audio/")) return "audio"; + return null; +} + +function validateFileSize(file: File): { valid: boolean; error?: string } { + const mediaType = getMediaTypeFromFile(file); + if (!mediaType) { + return { valid: false, error: `Unsupported file type: ${file.type}` }; + } + + const limit = FILE_SIZE_LIMITS[mediaType]; + if (file.size > limit) { + return { + valid: false, + error: `${file.name} (${formatFileSize(file.size)}) exceeds the ${formatFileSize(limit)} limit for ${mediaType} files`, + }; + } + + return { valid: true }; +} + export function MediaView() { const editor = useEditor(); const mediaFiles = editor.media.getAssets(); @@ -71,11 +110,41 @@ export function MediaView() { return; } + // Validate file sizes before processing + const validFiles: File[] = []; + const errors: string[] = []; + + for (const file of Array.from(files)) { + const validation = validateFileSize(file); + if (validation.valid) { + validFiles.push(file); + } else if (validation.error) { + errors.push(validation.error); + } + } + + // Show errors for rejected files + for (const error of errors) { + toast.error(error); + } + + // If no valid files, stop here + if (validFiles.length === 0) { + return; + } + + // Create a new FileList-like object from valid files + const dataTransfer = new DataTransfer(); + for (const file of validFiles) { + dataTransfer.items.add(file); + } + const validFileList = dataTransfer.files; + setIsProcessing(true); setProgress(0); try { const processedAssets = await processMediaAssets({ - files, + files: validFileList, onProgress: (progress: { progress: number }) => setProgress(progress.progress), });