Page MenuHomePhorge

No OneTemporary

Size
6 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Tue, Nov 26, 12:15 AM (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40122
Default Alt Text
(6 KB)

Event Timeline