/** * Minimal extension ↔ MIME mapping + `accept` matching. Zero-dep: just a table * big enough for the common upload types (images, docs, media, archives). */ const BY_EXT: Record = { // images png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", avif: "image/avif", svg: "image/svg+xml", ico: "image/x-icon", bmp: "image/bmp", // documents pdf: "application/pdf", txt: "text/plain", html: "text/html", htm: "text/html", csv: "text/csv", json: "application/json", xml: "application/xml", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", // media mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", mp4: "video/mp4", webm: "video/webm", mov: "video/quicktime", // archives / misc zip: "application/zip", gz: "application/gzip", tar: "application/x-tar", }; /** Lowercased extension WITHOUT the dot (e.g. "png"), or "" if none. */ export function extOf(name: string): string { const clean = name.split(/[?#]/)[0] ?? ""; const dot = clean.lastIndexOf("."); return dot >= 0 ? clean.slice(dot + 1).toLowerCase() : ""; } /** MIME type for a filename/key by its extension, or a safe default. */ export function contentTypeOf(name: string, fallback = "application/octet-stream"): string { return BY_EXT[extOf(name)] ?? fallback; } /** The conventional extension for a MIME type, or "" (used to name S3 keys). */ export function extForType(type: string): string { const t = type.split(";")[0]!.trim().toLowerCase(); for (const [ext, mime] of Object.entries(BY_EXT)) if (mime === t) return ext; return ""; } /** * Does `file` (its MIME `type` + `name`) satisfy an `accept` list? Each accept * entry is a MIME type (`"image/png"`), a wildcard MIME (`"image/*"`), or a * dotted extension (`".pdf"`). An empty/omitted list accepts everything. */ export function accepts( accept: string[] | undefined, file: { type: string; name: string }, ): boolean { if (!accept || accept.length === 0) return true; const type = (file.type || contentTypeOf(file.name)).toLowerCase(); const ext = "." + extOf(file.name); return accept.some((raw) => { const rule = raw.trim().toLowerCase(); if (rule.startsWith(".")) return rule === ext; if (rule.endsWith("/*")) return type.startsWith(rule.slice(0, -1)); // "image/" prefix return rule === type; }); }