perf(web): optimize document upload validation with static sets, MIME checks, and 50MB size guard

- Hoist ALLOWED_EXTENSIONS and ALLOWED_MIME_TYPES Sets to module scope so
  batch drag-and-drop no longer re-allocates them per file check
- Fail fast on files larger than 50MB (matching the chat attachment and
  backend worker limit) with a dedicated toast instead of uploading them
  only to be rejected at the gateway
- Verify MIME types directly (application/pdf, Office types, text/csv,
  text/plain, ...) so extensionless uploads such as mobile blobs are
  accepted, stripping parameters like ";charset=utf-8" before lookup,
  with the extension check kept as fallback
- Add application/pdf to FILE_ACCEPT, matching CHAT_ATTACHMENT_ACCEPT

Fixes #1559

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Muskan Jain 2026-08-27 20:57:51 +07:00
parent f11d8c4620
commit 90d52a2883

View file

@ -8,7 +8,9 @@ import { useHotkeys } from "react-hotkeys-hook"
import { toast } from "sonner"
export const FILE_ACCEPT =
"image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.md,.mdx,.json,.html,.htm,text/markdown,application/json,text/html"
"image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.md,.mdx,.json,.html,.htm,application/pdf,text/markdown,application/json,text/html"
export const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024
export type FileQueueItemStatus = "pending" | "uploading" | "success" | "error"
@ -33,29 +35,50 @@ interface FileContentProps {
isOpen?: boolean
}
function isAcceptedFile(file: File): boolean {
const ALLOWED_EXTENSIONS = new Set([
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
".txt",
".md",
".mdx",
".json",
".html",
".htm",
])
const ALLOWED_MIME_TYPES = new Set([
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/csv",
"text/plain",
"text/markdown",
"application/json",
"text/html",
])
export function isAcceptedFile(file: File): boolean {
if (file.size > MAX_FILE_SIZE_BYTES) return false
if (file.type) {
// Drop parameters like ";charset=utf-8" before the exact lookup
const semicolonIndex = file.type.indexOf(";")
const mimeType = (
semicolonIndex === -1 ? file.type : file.type.slice(0, semicolonIndex)
)
.trim()
.toLowerCase()
if (ALLOWED_MIME_TYPES.has(mimeType)) return true
if (mimeType.startsWith("image/")) return true
}
const name = file.name.toLowerCase()
const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")) : ""
const allowedExt = new Set([
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
".txt",
".md",
".mdx",
".json",
".html",
".htm",
])
if (allowedExt.has(ext)) return true
if (file.type.startsWith("image/")) return true
if (file.type === "text/markdown") return true
if (file.type === "application/json") return true
if (file.type === "text/html") return true
return false
return ALLOWED_EXTENSIONS.has(ext)
}
function fileQueueKey(file: File): string {
@ -100,13 +123,30 @@ export function FileContent({
const addFiles = useCallback(
(fileList: FileList | File[]) => {
const incoming = Array.from(fileList)
const accepted = incoming.filter(isAcceptedFile)
const rejected = incoming.length - accepted.length
if (rejected > 0) {
const accepted: File[] = []
let oversizedCount = 0
let unsupportedCount = 0
for (const file of incoming) {
if (file.size > MAX_FILE_SIZE_BYTES) {
oversizedCount++
} else if (isAcceptedFile(file)) {
accepted.push(file)
} else {
unsupportedCount++
}
}
if (oversizedCount > 0) {
toast.error(
rejected === 1
oversizedCount === 1
? `One file exceeds the ${formatFileSize(MAX_FILE_SIZE_BYTES)} limit`
: `${oversizedCount} files exceed the ${formatFileSize(MAX_FILE_SIZE_BYTES)} limit`,
)
}
if (unsupportedCount > 0) {
toast.error(
unsupportedCount === 1
? "One file type is not supported"
: `${rejected} files are not supported`,
: `${unsupportedCount} files are not supported`,
)
}
if (accepted.length === 0) return