Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F114231
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
6 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/majic/plug.ex b/lib/majic/plug.ex
new file mode 100644
index 0000000..a006e73
--- /dev/null
+++ b/lib/majic/plug.ex
@@ -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
diff --git a/test/fixtures/cat.webp b/test/fixtures/cat.webp
new file mode 100644
index 0000000..93d0e8f
Binary files /dev/null and b/test/fixtures/cat.webp differ
diff --git a/test/majic/plug_test.exs b/test/majic/plug_test.exs
new file mode 100644
index 0000000..a850cb5
--- /dev/null
+++ b/test/majic/plug_test.exs
@@ -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
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Nov 26, 12:15 AM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40122
Default Alt Text
(6 KB)
Attached To
Mode
R20 majic
Attached
Detach File
Event Timeline
Log In to Comment