This commit is contained in:
Jordan Bracco 2020-06-16 11:55:13 +02:00
parent debcadf495
commit 91fdf2303b
3 changed files with 205 additions and 0 deletions

133
lib/majic/plug.ex Normal file
View file

@ -0,0 +1,133 @@
if Code.ensure_loaded?(Plug) do
defmodule Majic.PlugError do
defexception [:message]
end
defmodule Majic.Plug do
@moduledoc """
A `Plug` to automatically set the `content_type` of every `Plug.Upload`.
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
To use a gen_magic pool:
```
plug Majic.Plug, pool: MyApp.MajicPool
```
To use a single gen_magic server:
```
plug Majic.Plug, server: MyApp.MajicServer
```
To start a gen_magic process at each file (not recommended):
```
plug Majic.Plug, once: true
```
"""
@behaviour Plug
@impl Plug
def init(opts) do
mod =
cond do
Keyword.has_key?(opts, :pool) -> {Majic.Pool, Keyword.get(opts, :pool)}
Keyword.has_key?(opts, :server) -> {Majic.Server, Keyword.get(opts, :server)}
Keyword.has_key?(opts, :once) -> {Majic.Once, nil}
true -> raise(Majic.PlugError, "No server/pool/once option defined")
end
opts
|> Keyword.put(:__module__, mod)
|> Keyword.put_new(:fix_extension, true)
|> Keyword.put_new(:append_extension, false)
end
@impl Plug
def call(%{params: params} = conn, opts) do
%{conn | params: collect_uploads(params, opts)}
end
defp collect_uploads(params, opts) do
Enum.reduce(params, Map.new(), fn value, acc -> collect_upload(value, acc, opts) end)
end
defp collect_upload({k, %{__struct__: Plug.Upload, path: path} = upload}, acc, opts) do
case perform(Keyword.get(opts, :__module__), path) do
{:ok, magic} ->
Map.put(acc, k, fix_upload(upload, magic, opts))
{:error, error} ->
raise(Majic.PlugError, "Failed to gen_magic: #{inspect(error)}")
end
end
defp collect_upload({k, v}, acc, opts) when is_map(v) do
Map.put(acc, k, collect_uploads(v, opts))
end
defp collect_upload({k, v}, acc, _opts) do
Map.put(acc, k, v)
end
defp perform({mod = Majic.Once, _}, path) do
mod.perform(path)
end
defp perform({mod, name}, path) do
mod.perform(name, path)
end
defp fix_upload(upload, magic, opts) do
%{upload | content_type: magic.mime_type}
|> fix_extension(Keyword.get(opts, :fix_extension), opts)
end
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
defp fix_extension(upload, _, _) do
upload
end
defp rewrite_extension(upload, _old, [], _opts) 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
defp rewrite_or_append_extension(basename, "." <> old, ext, true) do
Enum.join([basename, old, ext], ".")
end
defp rewrite_or_append_extension(basename, _, ext, _) do
basename <> "." <> ext
end
end
end

BIN
test/fixtures/cat.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

72
test/majic/plug_test.exs Normal file
View file

@ -0,0 +1,72 @@
defmodule Majic.PlugTest do
use ExUnit.Case, async: true
use Plug.Test
defmodule TestRouter do
use Plug.Router
plug :match
plug :dispatch
plug Plug.Parsers,
parsers: [:urlencoded, :multipart],
pass: ["*/*"]
#plug Majic.Plug, once: true
post "/" do
send_resp(conn, 200, "Ok")
end
end
setup_all do
Application.ensure_all_started(:plug)
:ok
end
@router_opts TestRouter.init([])
test "convert uploads" do
multipart = """
------w58EW1cEpjzydSCq\r
Content-Disposition: form-data; name=\"form[makefile]\"; filename*=\"utf-8''mymakefile.txt\"\r
Content-Type: text/plain\r
\r
#{String.replace(File.read!("Makefile"), "\n", "\n")}\r
------w58EW1cEpjzydSCq\r
Content-Disposition: form-data; name=\"form[make][file]\"; filename*=\"utf-8''mymakefile.txt\"\r
Content-Type: text/plain\r
\r
#{String.replace(File.read!("Makefile"), "\n", "\n")}\r
------w58EW1cEpjzydSCq\r
Content-Disposition: form-data; name=\"cat\"; filename*=\"utf-8''cute-cat.jpg\"\r
Content-Type: image/jpg\r
\r
#{String.replace(File.read!("test/fixtures/cat.webp"), "\n", "\n")}\r
------w58EW1cEpjzydSCq--\r
"""
orig_conn = conn(:post, "/", multipart)
|> put_req_header("content-type", "multipart/mixed; boundary=----w58EW1cEpjzydSCq")
|> TestRouter.call(@router_opts)
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])
conn = Majic.Plug.call(orig_conn, plug)
conn_no_ext = Majic.Plug.call(orig_conn, plug_no_ext)
conn_append_ext = Majic.Plug.call(orig_conn, plug_append_ext)
assert conn.state == :sent
assert conn.status == 200
refute get_in(conn.body_params, ["form", "makefile"]).content_type == get_in(conn.params, ["form", "makefile"]).content_type
assert get_in(conn.params, ["form", "makefile"]).content_type == "text/x-makefile"
refute get_in(conn.body_params, ["form", "make", "file"]).content_type == get_in(conn.params, ["form", "make", "file"]).content_type
assert get_in(conn.params, ["form", "make", "file"]).content_type == "text/x-makefile"
refute get_in(conn.body_params, ["cat"]).content_type == get_in(conn.params, ["cat"]).content_type
assert get_in(conn.params, ["cat"]).content_type == "image/webp"
assert get_in(conn.params, ["cat"]).filename == "cute-cat.webp"
assert get_in(conn_no_ext.params, ["cat"]).filename == "cute-cat.jpg"
assert get_in(conn_append_ext.params, ["cat"]).filename == "cute-cat.jpg.webp"
end
end