mime.ex (7438B)
1 defmodule MIME do 2 @moduledoc """ 3 Maps MIME types to its file extensions and vice versa. 4 5 MIME types can be extended in your application configuration 6 as follows: 7 8 config :mime, :types, %{ 9 "application/vnd.api+json" => ["json-api"] 10 } 11 12 After adding the configuration, MIME needs to be recompiled. 13 If you are using mix, it can be done with: 14 15 $ mix deps.clean mime --build 16 17 """ 18 19 types = %{ 20 "application/atom+xml" => ["atom"], 21 "application/epub+zip" => ["epub"], 22 "application/gzip" => ["gz"], 23 "application/java-archive" => ["jar"], 24 "application/javascript" => ["js"], 25 "application/json" => ["json"], 26 "application/json-patch+json" => ["json-patch"], 27 "application/ld+json" => ["jsonld"], 28 "application/manifest+json" => ["webmanifest"], 29 "application/msword" => ["doc"], 30 "application/octet-stream" => ["bin"], 31 "application/ogg" => ["ogx"], 32 "application/pdf" => ["pdf"], 33 "application/postscript" => ["ps", "eps", "ai"], 34 "application/rss+xml" => ["rss"], 35 "application/rtf" => ["rtf"], 36 "application/vnd.amazon.ebook" => ["azw"], 37 "application/vnd.api+json" => ["json-api"], 38 "application/vnd.apple.installer+xml" => ["mpkg"], 39 "application/vnd.etsi.asic-e+zip" => ["asice", "sce"], 40 "application/vnd.etsi.asic-s+zip" => ["asics", "scs"], 41 "application/vnd.mozilla.xul+xml" => ["xul"], 42 "application/vnd.ms-excel" => ["xls"], 43 "application/vnd.ms-fontobject" => ["eot"], 44 "application/vnd.ms-powerpoint" => ["ppt"], 45 "application/vnd.oasis.opendocument.presentation" => ["odp"], 46 "application/vnd.oasis.opendocument.spreadsheet" => ["ods"], 47 "application/vnd.oasis.opendocument.text" => ["odt"], 48 "application/vnd.openxmlformats-officedocument.presentationml.presentation" => ["pptx"], 49 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => ["xlsx"], 50 "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => ["docx"], 51 "application/vnd.rar" => ["rar"], 52 "application/vnd.visio" => ["vsd"], 53 "application/wasm" => ["wasm"], 54 "application/x-7z-compressed" => ["7z"], 55 "application/x-abiword" => ["abw"], 56 "application/x-bzip" => ["bz"], 57 "application/x-bzip2" => ["bz2"], 58 "application/x-cdf" => ["cda"], 59 "application/x-csh" => ["csh"], 60 "application/x-freearc" => ["arc"], 61 "application/x-httpd-php" => ["php"], 62 "application/x-msaccess" => ["mdb"], 63 "application/x-sh" => ["sh"], 64 "application/x-shockwave-flash" => ["swf"], 65 "application/x-tar" => ["tar"], 66 "application/xhtml+xml" => ["xhtml"], 67 "application/xml" => ["xml"], 68 "application/zip" => ["zip"], 69 "audio/3gpp" => ["3gp"], 70 "audio/3gpp2" => ["3g2"], 71 "audio/aac" => ["aac"], 72 "audio/midi" => ["mid", "midi"], 73 "audio/mpeg" => ["mp3"], 74 "audio/ogg" => ["oga"], 75 "audio/opus" => ["opus"], 76 "audio/wav" => ["wav"], 77 "audio/webm" => ["weba"], 78 "font/otf" => ["otf"], 79 "font/ttf" => ["ttf"], 80 "font/woff" => ["woff"], 81 "font/woff2" => ["woff2"], 82 "image/avif" => ["avif"], 83 "image/bmp" => ["bmp"], 84 "image/gif" => ["gif"], 85 "image/heic" => ["heic"], 86 "image/heif" => ["heif"], 87 "image/jpeg" => ["jpg", "jpeg"], 88 "image/jxl" => ["jxl"], 89 "image/png" => ["png"], 90 "image/svg+xml" => ["svg", "svgz"], 91 "image/tiff" => ["tiff", "tif"], 92 "image/vnd.adobe.photoshop" => ["psd"], 93 "image/vnd.microsoft.icon" => ["ico"], 94 "image/webp" => ["webp"], 95 "text/calendar" => ["ics"], 96 "text/css" => ["css"], 97 "text/csv" => ["csv"], 98 "text/html" => ["html", "htm"], 99 "text/javascript" => ["js", "mjs"], 100 "text/markdown" => ["md", "markdown"], 101 "text/plain" => ["txt", "text"], 102 "text/xml" => ["xml"], 103 "video/3gpp" => ["3gp"], 104 "video/3gpp2" => ["3g2"], 105 "video/mp2t" => ["ts"], 106 "video/mp4" => ["mp4"], 107 "video/mpeg" => ["mpeg", "mpg"], 108 "video/ogg" => ["ogv"], 109 "video/quicktime" => ["mov"], 110 "video/webm" => ["webm"], 111 "video/x-ms-wmv" => ["wmv"], 112 "video/x-msvideo" => ["avi"] 113 } 114 115 require Application 116 custom_types = Application.compile_env(:mime, :types, %{}) 117 118 to_exts = fn map -> 119 for {media, exts} <- map, ext <- exts, reduce: %{} do 120 acc -> Map.update(acc, ext, [media], &[media | &1]) 121 end 122 end 123 124 exts = 125 Map.merge(to_exts.(types), %{ 126 "3g2" => ["video/3gpp2"], 127 "3gp" => ["video/3gpp"], 128 "js" => ["text/javascript"], 129 "xml" => ["text/xml"] 130 }) 131 132 for {ext, [_, _ | _] = mimes} <- exts do 133 raise "conflicting MIMEs for extension .#{ext}, please override: #{inspect(mimes)}" 134 end 135 136 all_exts = Map.merge(exts, to_exts.(custom_types)) 137 all_types = Map.merge(types, custom_types) 138 139 @doc """ 140 Returns the custom types compiled into the MIME module. 141 """ 142 def compiled_custom_types do 143 unquote(Macro.escape(custom_types)) 144 end 145 146 @doc """ 147 Returns the extensions associated with a given MIME type. 148 149 ## Examples 150 151 iex> MIME.extensions("text/html") 152 ["html", "htm"] 153 154 iex> MIME.extensions("application/json") 155 ["json"] 156 157 iex> MIME.extensions("application/vnd.custom+xml") 158 ["xml"] 159 160 iex> MIME.extensions("foo/bar") 161 [] 162 163 """ 164 @spec extensions(String.t()) :: [String.t()] 165 def extensions(type) do 166 mime = 167 type 168 |> strip_params() 169 |> downcase("") 170 171 mime_to_ext(mime) || suffix(mime) || [] 172 end 173 174 defp suffix(type) do 175 case String.split(type, "+") do 176 [_type_subtype_without_suffix, suffix] -> [suffix] 177 _ -> nil 178 end 179 end 180 181 @default_type "application/octet-stream" 182 183 @doc """ 184 Returns the MIME type associated with a file extension. 185 186 If no MIME type is known for `file_extension`, 187 `#{inspect(@default_type)}` is returned. 188 189 ## Examples 190 191 iex> MIME.type("txt") 192 "text/plain" 193 194 iex> MIME.type("foobarbaz") 195 #{inspect(@default_type)} 196 197 """ 198 @spec type(String.t()) :: String.t() 199 def type(file_extension) do 200 ext_to_mime(file_extension) || @default_type 201 end 202 203 @doc """ 204 Returns whether an extension has a MIME type registered. 205 206 ## Examples 207 208 iex> MIME.has_type?("txt") 209 true 210 211 iex> MIME.has_type?("foobarbaz") 212 false 213 214 """ 215 @spec has_type?(String.t()) :: boolean 216 def has_type?(file_extension) do 217 is_binary(ext_to_mime(file_extension)) 218 end 219 220 @doc """ 221 Guesses the MIME type based on the path's extension. See `type/1`. 222 223 ## Examples 224 225 iex> MIME.from_path("index.html") 226 "text/html" 227 228 """ 229 @spec from_path(Path.t()) :: String.t() 230 def from_path(path) do 231 case Path.extname(path) do 232 "." <> ext -> type(downcase(ext, "")) 233 _ -> @default_type 234 end 235 end 236 237 defp strip_params(string) do 238 string |> :binary.split(";") |> hd() 239 end 240 241 defp downcase(<<h, t::binary>>, acc) when h in ?A..?Z, 242 do: downcase(t, <<acc::binary, h + 32>>) 243 244 defp downcase(<<h, t::binary>>, acc), do: downcase(t, <<acc::binary, h>>) 245 defp downcase(<<>>, acc), do: acc 246 247 @spec ext_to_mime(String.t()) :: String.t() | nil 248 defp ext_to_mime(type) 249 250 for {ext, [type | _]} <- all_exts do 251 defp ext_to_mime(unquote(ext)), do: unquote(type) 252 end 253 254 defp ext_to_mime(_ext), do: nil 255 256 @spec mime_to_ext(String.t()) :: list(String.t()) | nil 257 defp mime_to_ext(type) 258 259 for {type, exts} <- all_types do 260 defp mime_to_ext(unquote(type)), do: unquote(List.wrap(exts)) 261 end 262 263 defp mime_to_ext(_type), do: nil 264 end