Majic.Extension
This commit is contained in:
parent
d5dbd1a0c6
commit
4983596ab2
6 changed files with 163 additions and 76 deletions
13
README.md
13
README.md
|
@ -145,6 +145,16 @@ And then you can use it with `Majic.perform/2` with `pool: YourApp.MajicPool` op
|
|||
iex(1)> Majic.perform(Path.expand("~/.bash_history"), pool: YourApp.MajicPool)
|
||||
{:ok, %Majic.Result{mime_type: "text/plain", encoding: "us-ascii", content: "ASCII text"}}
|
||||
```
|
||||
#### Fixing extensions
|
||||
|
||||
You may also want to fix the user-provided filename according to its detected MIME type. To do this, you can use `Majic.Extension.fix/3`:
|
||||
|
||||
```elixir
|
||||
iex(1)> {:ok, result} = Majic.perform("cat.jpeg", once: true)
|
||||
{:ok, %Majic.Result{mime_type: "image/webp", ...}}
|
||||
iex(1)> Majic.Extension.fix("cat.jpeg", result)
|
||||
"cat.webp"
|
||||
```
|
||||
|
||||
#### Use with Plug.Upload
|
||||
|
||||
|
@ -152,7 +162,8 @@ If you use Plug or Phoenix, you may want to automatically verify the content typ
|
|||
`Majic.Plug` is there for this.
|
||||
|
||||
Enable it by using `plug Majic.Plug, pool: YourApp.MajicPool` in your pipeline or controller. Then, every `Plug.Upload`
|
||||
in `conn.params` is now verified. The filename is also altered with an extension matching its content-type.
|
||||
in `conn.params` and `conn.body_params` is now verified. The filename is also altered with an extension matching its
|
||||
content-type, using `Majic.Extension`.
|
||||
|
||||
## Notes
|
||||
|
||||
|
|
89
lib/majic/extension.ex
Normal file
89
lib/majic/extension.ex
Normal file
|
@ -0,0 +1,89 @@
|
|||
defmodule Majic.Extension do
|
||||
@moduledoc """
|
||||
Helper module to fix extensions. Uses [MIME](https://hexdocs.pm/mime/MIME.html).
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
If an extension is defined for a given MIME type, append it to the previous extension.
|
||||
|
||||
If no extension could be found for the MIME type, and `subtype_as_extension: false`, the returned filename will have no extension.
|
||||
"""
|
||||
@type option_append :: {:append, false | true}
|
||||
|
||||
@typedoc "If no extension is defined for a given MIME type, use the subtype as its extension."
|
||||
@type option_subtype_as_extension :: {:subtype_as_extension, false | true}
|
||||
|
||||
@spec fix(Path.t(), Majic.Result.t() | String.t(), [
|
||||
option_append() | option_subtype_as_extension()
|
||||
]) :: Path.t()
|
||||
@doc """
|
||||
Fix `name`'s extension according to `result_or_mime_type`.
|
||||
|
||||
```elixir
|
||||
iex(1)> {:ok, result} = Majic.perform("cat.jpeg", once: true)
|
||||
{:ok, %Majic.Result{mime_type: "image/webp", ...}}
|
||||
iex(1)> Majic.Extension.fix("cat.jpeg", result)
|
||||
"cat.webp"
|
||||
```
|
||||
|
||||
The `append: true` option will append the correct extension to the user-provided one, if there's an extension for the
|
||||
type:
|
||||
|
||||
```
|
||||
iex(1)> Majic.Extension.fix("cat.jpeg", result, append: true)
|
||||
"cat.jpeg.webp"
|
||||
iex(2)> Majic.Extension.fix("Makefile.txt", "text/x-makefile", append: true)
|
||||
"Makefile"
|
||||
```
|
||||
|
||||
The `subtype_as_extension: true` option will use the subtype part of the MIME type as an extension for the ones that
|
||||
don't have any:
|
||||
|
||||
```elixir
|
||||
iex(1)> Majic.Extension.fix("Makefile.txt", "text/x-makefile", subtype_as_extension: true)
|
||||
"Makefile.x-makefile"
|
||||
iex(1)> Majic.Extension.fix("Makefile.txt", "text/x-makefile", subtype_as_extension: true, append: true)
|
||||
"Makefile.txt.x-makefile"
|
||||
```
|
||||
"""
|
||||
def fix(name, result_or_mime_type, options \\ [])
|
||||
|
||||
def fix(name, %Majic.Result{mime_type: mime_type}, options) do
|
||||
do_fix(name, mime_type, options)
|
||||
end
|
||||
|
||||
def fix(name, mime_type, options) do
|
||||
do_fix(name, mime_type, options)
|
||||
end
|
||||
|
||||
defp do_fix(name, mime_type, options) do
|
||||
append? = Keyword.get(options, :append, false)
|
||||
subtype? = Keyword.get(options, :subtype_as_extension, false)
|
||||
exts = MIME.extensions(mime_type) ++ subtype_extension(subtype?, mime_type)
|
||||
old_ext = String.downcase(Path.extname(name))
|
||||
basename = Path.basename(name, old_ext)
|
||||
"." <> old = old_ext
|
||||
|
||||
if old in exts do
|
||||
Enum.join([basename, old])
|
||||
else
|
||||
ext = List.first(exts)
|
||||
|
||||
ext_list =
|
||||
cond do
|
||||
ext && append? -> [old, ext]
|
||||
!ext -> []
|
||||
ext -> [ext]
|
||||
end
|
||||
|
||||
Enum.join([basename] ++ ext_list, ".")
|
||||
end
|
||||
end
|
||||
|
||||
defp subtype_extension(true, type) do
|
||||
[_type, sub] = String.split(type, "/", parts: 2)
|
||||
[sub]
|
||||
end
|
||||
|
||||
defp subtype_extension(_, _), do: []
|
||||
end
|
|
@ -10,22 +10,22 @@ if Code.ensure_loaded?(Plug) do
|
|||
One of the required option of `pool`, `server` or `once` must be set.
|
||||
|
||||
Additional options:
|
||||
* `fix_extension`, default true: rewrite the user provided `filename` with a valid extension for the detected content type
|
||||
* `append_extension`, default false: append the valid extension to the previous filename, without removing the user provided extension
|
||||
* `fix_extension`, default true: enable use of `Majic.Extension`,
|
||||
* options for `Majic.Extension`.
|
||||
|
||||
To use a gen_magic pool:
|
||||
To use a majic pool:
|
||||
|
||||
```
|
||||
plug Majic.Plug, pool: MyApp.MajicPool
|
||||
```
|
||||
|
||||
To use a single gen_magic server:
|
||||
To use a single majic server:
|
||||
|
||||
```
|
||||
plug Majic.Plug, server: MyApp.MajicServer
|
||||
```
|
||||
|
||||
To start a gen_magic process at each file (not recommended):
|
||||
To start a majic process at each file (not recommended):
|
||||
|
||||
```
|
||||
plug Majic.Plug, once: true
|
||||
|
@ -44,7 +44,8 @@ if Code.ensure_loaded?(Plug) do
|
|||
|
||||
opts
|
||||
|> Keyword.put_new(:fix_extension, true)
|
||||
|> Keyword.put_new(:append_extension, false)
|
||||
|> Keyword.put_new(:append, false)
|
||||
|> Keyword.put_new(:subtype_as_extension, false)
|
||||
end
|
||||
|
||||
@impl Plug
|
||||
|
@ -105,71 +106,18 @@ if Code.ensure_loaded?(Plug) do
|
|||
end
|
||||
|
||||
defp fix_upload(upload, magic, opts) do
|
||||
%{upload | content_type: magic.mime_type}
|
||||
|> fix_extension(Keyword.get(opts, :fix_extension), opts)
|
||||
end
|
||||
filename =
|
||||
if Keyword.get(opts, :fix_extension) do
|
||||
IO.puts("FIXING EXTENSION #{inspect opts}")
|
||||
ext_opts = [
|
||||
append: Keyword.get(opts, :append, false),
|
||||
subtype_as_extension: Keyword.get(opts, :subtype_as_extension, false)
|
||||
]
|
||||
|
||||
defp fix_extension(upload, true, opts) do
|
||||
old_ext = String.downcase(Path.extname(upload.filename))
|
||||
extensions = MIME.extensions(upload.content_type)
|
||||
rewrite_extension(upload, old_ext, extensions, opts)
|
||||
end
|
||||
Majic.Extension.fix(upload.filename, magic, ext_opts)
|
||||
end
|
||||
|
||||
defp fix_extension(upload, _, _) do
|
||||
upload
|
||||
end
|
||||
|
||||
defp rewrite_extension(upload, old, [ext | _] = exts, opts) do
|
||||
if old in exts do
|
||||
upload
|
||||
else
|
||||
basename = Path.basename(upload.filename, old)
|
||||
|
||||
%{
|
||||
upload
|
||||
| filename:
|
||||
rewrite_or_append_extension(
|
||||
basename,
|
||||
old,
|
||||
ext,
|
||||
Keyword.get(opts, :append_extension)
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# No extension for type.
|
||||
defp rewrite_extension(upload, old, [], opts) do
|
||||
%{
|
||||
upload
|
||||
| filename:
|
||||
rewrite_or_append_extension(
|
||||
Path.basename(upload.filename, old),
|
||||
old,
|
||||
nil,
|
||||
Keyword.get(opts, :append_extension)
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
# Append, no extension for type: keep old extension
|
||||
defp rewrite_or_append_extension(basename, "." <> old, nil, true) do
|
||||
basename <> "." <> old
|
||||
end
|
||||
|
||||
# No extension for type: only keep basename
|
||||
defp rewrite_or_append_extension(basename, _, nil, _) do
|
||||
basename
|
||||
end
|
||||
|
||||
# Append
|
||||
defp rewrite_or_append_extension(basename, "." <> old, ext, true) do
|
||||
Enum.join([basename, old, ext], ".")
|
||||
end
|
||||
|
||||
# Rewrite
|
||||
defp rewrite_or_append_extension(basename, _, ext, _) do
|
||||
basename <> "." <> ext
|
||||
%{upload | content_type: magic.mime_type, filename: filename || upload.filename}
|
||||
end
|
||||
|
||||
# put value at path in conn.
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -43,8 +43,8 @@ defmodule Majic.MixProject do
|
|||
defp deps do
|
||||
[
|
||||
{:nimble_pool, "~> 0.1"},
|
||||
{:mime, "~> 1.0"},
|
||||
{:plug, "~> 1.0", optional: true},
|
||||
{:mime, "~> 1.0", optional: true},
|
||||
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
|
||||
{:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false},
|
||||
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
|
||||
|
|
33
test/majic/extension_test.exs
Normal file
33
test/majic/extension_test.exs
Normal file
|
@ -0,0 +1,33 @@
|
|||
defmodule Majic.ExtensionTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Majic.Extension
|
||||
|
||||
test "it fixes extensions" do
|
||||
assert "Makefile" == Extension.fix("Makefile.txt", "text/x-makefile")
|
||||
assert "cat.webp" == Extension.fix("cat.jpeg", "image/webp")
|
||||
end
|
||||
|
||||
test "it appends extensions" do
|
||||
assert "Makefile" == Extension.fix("Makefile.txt", "text/x-makefile", append: true)
|
||||
assert "cat.jpeg.webp" == Extension.fix("cat.jpeg", "image/webp", append: true)
|
||||
end
|
||||
|
||||
test "it uses subtype as extension" do
|
||||
assert "Makefile.x-makefile" ==
|
||||
Extension.fix("Makefile.txt", "text/x-makefile", subtype_as_extension: true)
|
||||
|
||||
assert "cat.webp" == Extension.fix("cat.jpeg", "image/webp", subtype_as_extension: true)
|
||||
end
|
||||
|
||||
test "it appends and use subtype" do
|
||||
assert "Makefile.txt.x-makefile" ==
|
||||
Extension.fix("Makefile.txt", "text/x-makefile",
|
||||
subtype_as_extension: true,
|
||||
append: true
|
||||
)
|
||||
|
||||
assert "cat.jpeg.webp" ==
|
||||
Extension.fix("cat.jpeg", "image/webp", subtype_as_extension: true, append: true)
|
||||
end
|
||||
end
|
|
@ -59,6 +59,11 @@ defmodule Majic.PlugTest do
|
|||
Content-Type: image/jpg\r
|
||||
\r
|
||||
#{File.read!("test/fixtures/cat.webp")}\r
|
||||
------w58EW1cEpjzydSCq\r
|
||||
Content-Disposition: form-data; name=\"cats[][inception][cat]\"; filename*=\"utf-8''third-cute-cat.jpg\"\r
|
||||
Content-Type: image/jpg\r
|
||||
\r
|
||||
#{File.read!("test/fixtures/cat.webp")}\r
|
||||
------w58EW1cEpjzydSCq--\r
|
||||
"""
|
||||
|
||||
|
@ -69,7 +74,7 @@ defmodule Majic.PlugTest do
|
|||
|
||||
plug = Majic.Plug.init(once: true)
|
||||
plug_no_ext = Majic.Plug.init(once: true, fix_extension: false)
|
||||
plug_append_ext = Majic.Plug.init(once: true, fix_extension: true, append_extension: true)
|
||||
plug_append_ext = Majic.Plug.init(once: true, fix_extension: true, append: true)
|
||||
|
||||
conn = Majic.Plug.call(orig_conn, plug)
|
||||
conn_no_ext = Majic.Plug.call(orig_conn, plug_no_ext)
|
||||
|
@ -84,7 +89,7 @@ defmodule Majic.PlugTest do
|
|||
assert get_in(conn.params, ["form", "makefile"]).content_type == "text/x-makefile"
|
||||
assert get_in(conn.params, ["form", "makefile"]).filename == "mymakefile"
|
||||
assert get_in(conn_no_ext.params, ["form", "makefile"]).filename == "mymakefile.txt"
|
||||
assert get_in(conn_append_ext.params, ["form", "makefile"]).filename == "mymakefile.txt"
|
||||
assert get_in(conn_append_ext.params, ["form", "makefile"]).filename == "mymakefile"
|
||||
|
||||
assert get_in(conn.body_params, ["form", "make", "file"]) ==
|
||||
get_in(conn.params, ["form", "make", "file"])
|
||||
|
@ -98,8 +103,9 @@ defmodule Majic.PlugTest do
|
|||
assert get_in(conn_append_ext.params, ["cat"]).filename == "cute-cat.jpg.webp"
|
||||
|
||||
assert Enum.all?(conn.params["cats"], fn
|
||||
%Plug.Upload{} = upload -> upload.content_type == "image/webp"
|
||||
_ -> true
|
||||
end)
|
||||
%Plug.Upload{} = upload -> upload.content_type == "image/webp"
|
||||
%{"inception" => %{"cat" => upload}} -> upload.content_type == "image/webp"
|
||||
_ -> true
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue