@@ -312,7 +312,6 @@ const EXTENSION_TO_MIME: Record<string, string> = {
312312 xls : 'application/vnd.ms-excel' ,
313313 ppt : 'application/vnd.ms-powerpoint' ,
314314 md : 'text/markdown' ,
315- jsonl : 'application/jsonl' ,
316315 yaml : 'application/x-yaml' ,
317316 yml : 'application/x-yaml' ,
318317 rtf : 'application/rtf' ,
@@ -363,15 +362,12 @@ const EXTENSION_TO_MIME: Record<string, string> = {
363362 graphql : 'text/x-graphql' ,
364363 gql : 'text/x-graphql' ,
365364 proto : 'text/x-protobuf' ,
366- mmd : 'text/x-mermaid' ,
367- diff : 'text/x-diff' ,
368- patch : 'text/x-diff' ,
369- fish : 'text/x-shellscript' ,
370365
371366 // Audio
372367 mp3 : 'audio/mpeg' ,
373368 m4a : 'audio/mp4' ,
374369 wav : 'audio/wav' ,
370+ webm : 'audio/webm' ,
375371 ogg : 'audio/ogg' ,
376372 flac : 'audio/flac' ,
377373 aac : 'audio/aac' ,
@@ -382,45 +378,80 @@ const EXTENSION_TO_MIME: Record<string, string> = {
382378 mov : 'video/quicktime' ,
383379 avi : 'video/x-msvideo' ,
384380 mkv : 'video/x-matroska' ,
385- // `.webm` is both an audio and a video container; the video type is the safe
386- // resolution because a `<video>` element plays an audio-only stream, while an
387- // `<audio>` element handed a video stream drops the picture.
388- webm : 'video/webm' ,
389381}
390382
391- /**
392- * MIME types that carry no format information. Storage keeps whatever the browser
393- * reported at upload time, and the direct-PUT path preserves it verbatim (see
394- * {@link getFileContentType}), so a stored type may be one of these even when the
395- * filename identifies the format precisely.
396- */
397- const GENERIC_MIME_TYPES = new Set ( [ 'application/octet-stream' , 'binary/octet-stream' ] )
383+ const GENERIC_MIME_TYPE = 'application/octet-stream'
384+
385+ /** Every MIME type that identifies no format, including the legacy `binary/` spelling. */
386+ const GENERIC_MIME_TYPES = new Set ( [ GENERIC_MIME_TYPE , 'binary/octet-stream' ] )
398387
399388/**
400389 * Get MIME type from file extension (fallback if not provided)
401390 */
402391export function getMimeTypeFromExtension ( extension : string ) : string {
403- return EXTENSION_TO_MIME [ extension . toLowerCase ( ) ] || 'application/octet-stream'
392+ return EXTENSION_TO_MIME [ extension . toLowerCase ( ) ] || GENERIC_MIME_TYPE
393+ }
394+
395+ /**
396+ * The MIME type that best identifies `filename`, preferring `declaredType` — by a browser
397+ * at upload time or by storage at read time — and falling back to the extension when what
398+ * was declared identifies no format.
399+ *
400+ * A declared `application/octet-stream` is not an error: browsers report it for plenty of
401+ * real formats, and the presigned PUT handshake requires persisting it verbatim (see
402+ * {@link getFileContentType}). A stored type therefore has to be resolved here before it
403+ * can drive rendering — a truthiness check (`file.type || fallback`) passes the generic
404+ * type straight through, and a `Blob` or media element handed that renders nothing.
405+ */
406+ export function resolveEffectiveMimeType (
407+ declaredType : string | null | undefined ,
408+ filename : string
409+ ) : string {
410+ const declared = declaredType ?. trim ( )
411+ if ( declared && ! GENERIC_MIME_TYPES . has ( declared ) ) return declared
412+ return getMimeTypeFromExtension ( getFileExtension ( filename ) )
413+ }
414+
415+ const MEDIA_FALLBACK_MIME = { audio : 'audio/mpeg' , video : 'video/mp4' } as const
416+
417+ /**
418+ * The MIME type to hand an `<audio>`/`<video>` element, given which of the two the caller
419+ * is rendering.
420+ *
421+ * Beyond {@link resolveEffectiveMimeType} this settles an ambiguity a filename alone cannot:
422+ * `.webm` and `.ogg` are both audio and video containers, so a resolved `audio/webm` would
423+ * make a `<video>` element drop the picture. The caller has already chosen the element, so
424+ * the container subtype is kept and retagged to that kind — the choice belongs here, where
425+ * the kind is known, and not in the extension table, which several non-viewer callers share.
426+ *
427+ * A type naming no media format falls back to the kind's default: passed through, it would
428+ * leave the element unable to determine the format, rendering nothing.
429+ */
430+ export function resolveMediaMimeType (
431+ declaredType : string | null | undefined ,
432+ filename : string ,
433+ kind : 'audio' | 'video'
434+ ) : string {
435+ const resolved = resolveEffectiveMimeType ( declaredType , filename )
436+ const [ type , subtype ] = resolved . split ( '/' )
437+ if ( type === kind ) return resolved
438+ if ( type === 'audio' || type === 'video' ) return `${ kind } /${ subtype } `
439+ return MEDIA_FALLBACK_MIME [ kind ]
404440}
405441
406442/**
407443 * Resolve a reliable MIME type from a file, falling back to the extension map
408- * when the browser reports an empty type. By default treats
409- * `application/octet-stream` as "unknown" and falls back to the extension —
410- * pass `{ preserveOctetStream: true }` for direct PUT uploads where the
444+ * when the browser reports an empty or generic type. Pass
445+ * `{ preserveOctetStream: true }` for direct PUT uploads where the
411446 * browser-supplied content-type must match the presigned handshake exactly.
412447 */
413448export function resolveFileType (
414449 file : { type : string ; name : string } ,
415450 options ?: { preserveOctetStream ?: boolean }
416451) : string {
417452 const browserType = file . type ?. trim ( )
418- if ( browserType ) {
419- if ( options ?. preserveOctetStream || browserType !== 'application/octet-stream' ) {
420- return browserType
421- }
422- }
423- return getMimeTypeFromExtension ( getFileExtension ( file . name ) )
453+ if ( browserType && options ?. preserveOctetStream ) return browserType
454+ return resolveEffectiveMimeType ( browserType , file . name )
424455}
425456
426457/**
@@ -432,32 +463,6 @@ export function getFileContentType(file: File): string {
432463 return resolveFileType ( file , { preserveOctetStream : true } )
433464}
434465
435- /**
436- * The MIME type to render a *stored* file as, resolving the generic types that storage
437- * legitimately holds against the filename.
438- *
439- * A stored `application/octet-stream` is not an error — browsers report it for plenty of
440- * real formats, and the presigned PUT handshake requires persisting it verbatim. Any
441- * consumer that feeds a stored type to a `Blob`, a media element, or a type filter must
442- * go through this rather than trusting `file.type` directly: a truthiness check
443- * (`file.type || fallback`) passes `application/octet-stream` straight through, and a
444- * media element handed that blob cannot determine the format and renders nothing.
445- *
446- * Returns `null` only when neither the stored type nor the extension identifies the file.
447- */
448- export function resolveEffectiveMimeType (
449- storedType : string | null | undefined ,
450- filename : string
451- ) : string | null {
452- const stored = storedType ?. trim ( )
453- if ( stored && ! GENERIC_MIME_TYPES . has ( stored ) ) return stored
454-
455- const fromExtension = getMimeTypeFromExtension ( getFileExtension ( filename ) )
456- if ( ! GENERIC_MIME_TYPES . has ( fromExtension ) ) return fromExtension
457-
458- return stored || null
459- }
460-
461466/**
462467 * Whether `error` is a DOM `AbortError` (XHR `abort()`, fetch `signal.aborted`,
463468 * etc). Used in upload retry loops so aborts short-circuit instead of retrying.
0 commit comments