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)
|
iex(1)> Majic.perform(Path.expand("~/.bash_history"), pool: YourApp.MajicPool)
|
||||||
{:ok, %Majic.Result{mime_type: "text/plain", encoding: "us-ascii", content: "ASCII text"}}
|
{: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
|
#### 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.
|
`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`
|
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
|
## 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.
|
One of the required option of `pool`, `server` or `once` must be set.
|
||||||
|
|
||||||
Additional options:
|
Additional options:
|
||||||
* `fix_extension`, default true: rewrite the user provided `filename` with a valid extension for the detected content type
|
* `fix_extension`, default true: enable use of `Majic.Extension`,
|
||||||
* `append_extension`, default false: append the valid extension to the previous filename, without removing the user provided extension
|
* options for `Majic.Extension`.
|
||||||
|
|
||||||
To use a gen_magic pool:
|
To use a majic pool:
|
||||||
|
|
||||||
```
|
```
|
||||||
plug Majic.Plug, pool: MyApp.MajicPool
|
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
|
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
|
plug Majic.Plug, once: true
|
||||||
|
@ -44,7 +44,8 @@ if Code.ensure_loaded?(Plug) do
|
||||||
|
|
||||||
opts
|
opts
|
||||||
|> Keyword.put_new(:fix_extension, true)
|
|> 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
|
end
|
||||||
|
|
||||||
@impl Plug
|
@impl Plug
|
||||||
|
@ -105,71 +106,18 @@ if Code.ensure_loaded?(Plug) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fix_upload(upload, magic, opts) do
|
defp fix_upload(upload, magic, opts) do
|
||||||
%{upload | content_type: magic.mime_type}
|
filename =
|
||||||
|> fix_extension(Keyword.get(opts, :fix_extension), opts)
|
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)
|
||||||
|
]
|
||||||
|
|
||||||
|
Majic.Extension.fix(upload.filename, magic, ext_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fix_extension(upload, true, opts) do
|
%{upload | content_type: magic.mime_type, filename: filename || upload.filename}
|
||||||
old_ext = String.downcase(Path.extname(upload.filename))
|
|
||||||
extensions = MIME.extensions(upload.content_type)
|
|
||||||
rewrite_extension(upload, old_ext, extensions, 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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# put value at path in conn.
|
# put value at path in conn.
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -43,8 +43,8 @@ defmodule Majic.MixProject do
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:nimble_pool, "~> 0.1"},
|
{:nimble_pool, "~> 0.1"},
|
||||||
|
{:mime, "~> 1.0"},
|
||||||
{:plug, "~> 1.0", optional: true},
|
{:plug, "~> 1.0", optional: true},
|
||||||
{:mime, "~> 1.0", optional: true},
|
|
||||||
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
|
||||||
{:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false},
|
{:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false},
|
||||||
{:ex_doc, ">= 0.0.0", 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
|
Content-Type: image/jpg\r
|
||||||
\r
|
\r
|
||||||
#{File.read!("test/fixtures/cat.webp")}\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
|
------w58EW1cEpjzydSCq--\r
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -69,7 +74,7 @@ defmodule Majic.PlugTest do
|
||||||
|
|
||||||
plug = Majic.Plug.init(once: true)
|
plug = Majic.Plug.init(once: true)
|
||||||
plug_no_ext = Majic.Plug.init(once: true, fix_extension: false)
|
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 = Majic.Plug.call(orig_conn, plug)
|
||||||
conn_no_ext = Majic.Plug.call(orig_conn, plug_no_ext)
|
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"]).content_type == "text/x-makefile"
|
||||||
assert get_in(conn.params, ["form", "makefile"]).filename == "mymakefile"
|
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_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"]) ==
|
assert get_in(conn.body_params, ["form", "make", "file"]) ==
|
||||||
get_in(conn.params, ["form", "make", "file"])
|
get_in(conn.params, ["form", "make", "file"])
|
||||||
|
@ -99,6 +104,7 @@ defmodule Majic.PlugTest do
|
||||||
|
|
||||||
assert Enum.all?(conn.params["cats"], fn
|
assert Enum.all?(conn.params["cats"], fn
|
||||||
%Plug.Upload{} = upload -> upload.content_type == "image/webp"
|
%Plug.Upload{} = upload -> upload.content_type == "image/webp"
|
||||||
|
%{"inception" => %{"cat" => upload}} -> upload.content_type == "image/webp"
|
||||||
_ -> true
|
_ -> true
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue