This commit is contained in:
Sruhvx-jpg 2026-08-26 04:03:36 +05:30 committed by GitHub
commit fdeb529ea8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 153 additions and 30 deletions

View file

@ -6,6 +6,10 @@ import { dmSansClassName } from "@/lib/fonts"
import { FileIcon, XIcon, AlertCircleIcon, CheckIcon } from "lucide-react"
import { useHotkeys } from "react-hotkeys-hook"
import { toast } from "sonner"
import {
isAcceptedFileType,
MAX_DOCUMENT_FILE_BYTES,
} from "@/lib/document-file-validation"
export const FILE_ACCEPT =
"image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.txt,.md,.mdx,.json,.html,.htm,text/markdown,application/json,text/html"
@ -33,31 +37,6 @@ interface FileContentProps {
isOpen?: boolean
}
function isAcceptedFile(file: File): boolean {
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
}
function fileQueueKey(file: File): string {
return `${file.name}:${file.size}:${file.lastModified}`
}
@ -100,13 +79,42 @@ 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 emptyCount = 0
let oversizedCount = 0
let unsupportedCount = 0
for (const file of incoming) {
if (file.size <= 0) {
emptyCount++
} else if (file.size > MAX_DOCUMENT_FILE_BYTES) {
oversizedCount++
} else if (!isAcceptedFileType(file)) {
unsupportedCount++
} else {
accepted.push(file)
}
}
if (emptyCount > 0) {
toast.error(
rejected === 1
emptyCount === 1
? "One file is empty"
: `${emptyCount} files are empty`,
)
}
if (oversizedCount > 0) {
toast.error(
oversizedCount === 1
? "One file exceeds the 50MB limit"
: `${oversizedCount} files exceed the 50MB 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

View file

@ -0,0 +1,68 @@
import { describe, expect, it } from "bun:test"
import {
isAcceptedFile,
isAcceptedFileType,
MAX_DOCUMENT_FILE_BYTES,
} from "./document-file-validation"
function createMockFile(name: string, size = 1024, type = ""): File {
return new File([new Uint8Array(size)], name, { type })
}
describe("document file validation", () => {
it("accepts standard documents by extension", () => {
expect(isAcceptedFile(createMockFile("document.pdf"))).toBe(true)
expect(isAcceptedFile(createMockFile("notes.md"))).toBe(true)
expect(isAcceptedFile(createMockFile("data.json"))).toBe(true)
expect(isAcceptedFile(createMockFile("sheet.xlsx"))).toBe(true)
expect(isAcceptedFile(createMockFile("report.docx"))).toBe(true)
expect(isAcceptedFile(createMockFile("data.csv"))).toBe(true)
})
it("accepts files with uppercase extensions and multi-dot filenames", () => {
expect(isAcceptedFile(createMockFile("DOCUMENT.PDF"))).toBe(true)
expect(isAcceptedFile(createMockFile("archive.v1.0.final.docx"))).toBe(true)
expect(isAcceptedFile(createMockFile("report.2026.08.19.csv"))).toBe(true)
})
it("accepts extensionless or generic files matching valid MIME types", () => {
expect(
isAcceptedFile(createMockFile("blob", 1024, "application/pdf")),
).toBe(true)
expect(
isAcceptedFile(createMockFile("uploaded-file", 1024, "application/json")),
).toBe(true)
expect(
isAcceptedFile(createMockFile("image-upload", 1024, "image/png")),
).toBe(true)
expect(isAcceptedFile(createMockFile("photo", 1024, "image/jpeg"))).toBe(
true,
)
})
it("correctly evaluates isAcceptedFileType independent of file size", () => {
expect(isAcceptedFileType(createMockFile("empty.pdf", 0))).toBe(true)
expect(
isAcceptedFileType(
createMockFile("large.pdf", MAX_DOCUMENT_FILE_BYTES + 1),
),
).toBe(true)
expect(isAcceptedFileType(createMockFile("script.sh"))).toBe(false)
})
it("rejects files exceeding the 50MB limit in isAcceptedFile", () => {
const oversized = MAX_DOCUMENT_FILE_BYTES + 1
expect(isAcceptedFile(createMockFile("large.pdf", oversized))).toBe(false)
})
it("rejects empty files with 0 bytes in isAcceptedFile", () => {
expect(isAcceptedFile(createMockFile("empty.pdf", 0))).toBe(false)
})
it("rejects unsupported extensions and executables", () => {
expect(isAcceptedFile(createMockFile("malware.exe"))).toBe(false)
expect(isAcceptedFile(createMockFile("script.sh"))).toBe(false)
expect(isAcceptedFile(createMockFile("archive.zip"))).toBe(false)
expect(isAcceptedFile(createMockFile("binary.bin"))).toBe(false)
})
})

View file

@ -0,0 +1,47 @@
export const MAX_DOCUMENT_FILE_BYTES = 50 * 1024 * 1024 // 50MB
export const ALLOWED_EXTENSIONS = new Set([
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".csv",
".txt",
".md",
".mdx",
".json",
".html",
".htm",
])
export const ALLOWED_MIME_TYPES = new Set([
"application/pdf",
"application/json",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/markdown",
"text/html",
"text/plain",
"text/csv",
])
export function isAcceptedFileType(file: File): boolean {
if (file.type) {
const baseMime = file.type.split(";")[0]?.trim().toLowerCase() ?? ""
if (baseMime.startsWith("image/")) return true
if (ALLOWED_MIME_TYPES.has(baseMime)) return true
}
const extIndex = file.name.lastIndexOf(".")
if (extIndex === -1) return false
return ALLOWED_EXTENSIONS.has(file.name.slice(extIndex).toLowerCase())
}
export function isAcceptedFile(file: File): boolean {
if (file.size <= 0 || file.size > MAX_DOCUMENT_FILE_BYTES) return false
return isAcceptedFileType(file)
}