Mime.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. type TypeMap = { [key: string]: string[] };
  2. export default class Mime {
  3. #extensionToType = new Map<string, string>();
  4. #typeToExtension = new Map<string, string>();
  5. #typeToExtensions = new Map<string, Set<string>>();
  6. constructor(...args: TypeMap[]) {
  7. for (const arg of args) {
  8. this.define(arg);
  9. }
  10. }
  11. /**
  12. * Define mimetype -> extension mappings. Each key is a mime-type that maps
  13. * to an array of extensions associated with the type. The first extension is
  14. * used as the default extension for the type.
  15. *
  16. * e.g. mime.define({'audio/ogg', ['oga', 'ogg', 'spx']});
  17. *
  18. * If a mapping for an extension has already been defined an error will be
  19. * thrown unless the `force` argument is set to `true`.
  20. *
  21. * e.g. mime.define({'audio/wav', ['wav']}, {'audio/x-wav', ['*wav']});
  22. */
  23. define(typeMap: TypeMap, force = false) {
  24. for (let [type, extensions] of Object.entries(typeMap)) {
  25. // Lowercase thingz
  26. type = type.toLowerCase();
  27. extensions = extensions.map((ext) => ext.toLowerCase());
  28. if (!this.#typeToExtensions.has(type)) {
  29. this.#typeToExtensions.set(type, new Set<string>());
  30. }
  31. const allExtensions = this.#typeToExtensions.get(type);
  32. let first = true;
  33. for (let extension of extensions) {
  34. const starred = extension.startsWith('*');
  35. extension = starred ? extension.slice(1) : extension;
  36. // Add to list of extensions for the type
  37. allExtensions?.add(extension);
  38. if (first) {
  39. // Map type to default extension (first in list)
  40. this.#typeToExtension.set(type, extension);
  41. }
  42. first = false;
  43. // Starred types are not eligible to be the default extension
  44. if (starred) continue;
  45. // Map extension to type
  46. const currentType = this.#extensionToType.get(extension);
  47. if (currentType && currentType != type && !force) {
  48. throw new Error(
  49. `"${type} -> ${extension}" conflicts with "${currentType} -> ${extension}". Pass \`force=true\` to override this definition.`,
  50. );
  51. }
  52. this.#extensionToType.set(extension, type);
  53. }
  54. }
  55. return this;
  56. }
  57. /**
  58. * Get mime type associated with an extension
  59. */
  60. getType(path: string) {
  61. if (typeof path !== 'string') return null;
  62. // Remove chars preceeding `/` or `\`
  63. const last = path.replace(/^.*[/\\]/, '').toLowerCase();
  64. // Remove chars preceeding '.'
  65. const ext = last.replace(/^.*\./, '').toLowerCase();
  66. const hasPath = last.length < path.length;
  67. const hasDot = ext.length < last.length - 1;
  68. // Extension-less file?
  69. if (!hasDot && hasPath) return null;
  70. return this.#extensionToType.get(ext) ?? null;
  71. }
  72. /**
  73. * Get default file extension associated with a mime type
  74. */
  75. getExtension(type: string) {
  76. if (typeof type !== 'string') return null;
  77. // Remove http header parameter(s) (specifically, charset)
  78. type = type?.split?.(';')[0];
  79. return (
  80. (type && this.#typeToExtension.get(type.trim().toLowerCase())) ?? null
  81. );
  82. }
  83. /**
  84. * Get all file extensions associated with a mime type
  85. */
  86. getAllExtensions(type: string) {
  87. if (typeof type !== 'string') return null;
  88. return this.#typeToExtensions.get(type.toLowerCase()) ?? null;
  89. }
  90. //
  91. // Private API, for internal use only. These APIs may change at any time
  92. //
  93. _freeze() {
  94. this.define = () => {
  95. throw new Error('define() not allowed for built-in Mime objects. See https://github.com/broofa/mime/blob/main/README.md#custom-mime-instances');
  96. };
  97. Object.freeze(this);
  98. for (const extensions of this.#typeToExtensions.values()) {
  99. Object.freeze(extensions);
  100. }
  101. return this;
  102. }
  103. _getTestState() {
  104. return {
  105. types: this.#extensionToType,
  106. extensions: this.#typeToExtension,
  107. };
  108. }
  109. }