Page MenuHomePhorge

No OneTemporary

Size
17 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/majic/extension.ex b/lib/majic/extension.ex
index e112b99..e701f79 100644
--- a/lib/majic/extension.ex
+++ b/lib/majic/extension.ex
@@ -1,115 +1,146 @@
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)
ext_candidates = MIME.extensions(mime_type)
old_ext = Path.extname(name)
old_ext_bare = String.trim_leading(String.downcase(old_ext), ".")
dir = Path.dirname(name)
basename = Path.basename(name, old_ext)
full_basename = Path.basename(name)
+ has_ext? = not match?("", old_ext)
join = fn filename ->
case dir do
"." -> filename
_ -> Path.join(dir, filename)
end
end
cond do
old_ext_bare in ext_candidates ->
name
- not match?("", old_ext) && append? && subtype? ->
+ has_ext? ->
+ fix_existing_ext(
+ basename,
+ full_basename,
+ append?,
+ subtype?,
+ ext_candidates,
+ mime_type,
+ join
+ )
+
+ true ->
+ fix_no_ext(name, basename, append?, subtype?, ext_candidates, mime_type, join)
+ end
+ end
+
+ defp fix_existing_ext(
+ basename,
+ full_basename,
+ append?,
+ subtype?,
+ ext_candidates,
+ mime_type,
+ join
+ ) do
+ cond do
+ append? && subtype? ->
join.(Enum.join([full_basename, subtype_extension(subtype?, mime_type)], "."))
- not match?("", old_ext) && subtype? ->
+ subtype? ->
join.(Enum.join([basename, subtype_extension(subtype?, mime_type)], "."))
- match?("", old_ext) && append? && not Enum.empty?(ext_candidates) ->
- join.(Enum.join([basename, List.first(ext_candidates)], "."))
+ append? && not Enum.empty?(ext_candidates) ->
+ join.(Enum.join([full_basename, List.first(ext_candidates)], "."))
- match?("", old_ext) && append? ->
- name
+ not Enum.empty?(ext_candidates) ->
+ join.(Enum.join([basename, List.first(ext_candidates)], "."))
- match?([], ext_candidates) ->
+ true ->
join.(basename)
+ end
+ end
- match?("", old_ext) ->
- name
-
- not Enum.empty?(ext_candidates) && append? ->
- join.(Enum.join([full_basename, List.first(ext_candidates)], "."))
+ defp fix_no_ext(name, basename, append?, subtype?, ext_candidates, mime_type, join) do
+ cond do
+ append? && subtype? ->
+ join.(Enum.join([basename, subtype_extension(subtype?, mime_type)], "."))
- not Enum.empty?(ext_candidates) ->
+ append? && not Enum.empty?(ext_candidates) ->
join.(Enum.join([basename, List.first(ext_candidates)], "."))
+ append? ->
+ name
+
true ->
name
end
end
defp subtype_extension(true, type) do
[_type, sub] = String.split(type, "/", parts: 2)
[sub]
end
defp subtype_extension(_, _), do: []
end
diff --git a/lib/majic/plug.ex b/lib/majic/plug.ex
index a302339..9a7233d 100644
--- a/lib/majic/plug.ex
+++ b/lib/majic/plug.ex
@@ -1,115 +1,116 @@
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 false: enable use of `Majic.Extension`,
* options for `Majic.Extension`.
To use a majic pool:
```
plug Majic.Plug, pool: MyApp.MajicPool
```
To use a single majic server:
```
plug Majic.Plug, server: MyApp.MajicServer
```
To start a majic process at each file (not recommended):
```
plug Majic.Plug, once: true
```
"""
@behaviour Plug
@impl Plug
def init(opts) do
cond do
Keyword.has_key?(opts, :pool) -> true
Keyword.has_key?(opts, :server) -> true
Keyword.has_key?(opts, :once) -> true
true -> raise(Majic.PlugError, "No server/pool/once option defined")
end
opts
|> Keyword.put_new(:fix_extension, false)
|> Keyword.put_new(:append, false)
|> Keyword.put_new(:subtype_as_extension, false)
end
@impl Plug
def call(conn, opts) do
body_params = transform_uploads(conn.body_params, opts)
params = merge_params(conn.query_params, body_params)
%{conn | body_params: body_params, params: params}
end
- defp merge_params(query_params, body_params) when is_map(query_params) and is_map(body_params) do
+ defp merge_params(query_params, body_params)
+ when is_map(query_params) and is_map(body_params) do
Map.merge(query_params, body_params, fn _k, qv, bv ->
merge_values(qv, bv)
end)
end
defp merge_values(qv, bv) when is_map(qv) and is_map(bv) do
Map.merge(qv, bv, fn _k, qv2, bv2 -> merge_values(qv2, bv2) end)
end
defp merge_values(_qv, bv), do: bv
defp transform_uploads(params, opts) when is_map(params) do
Map.new(params, fn {k, v} -> {k, transform_upload_value(v, opts)} end)
end
defp transform_uploads(params, opts) when is_list(params) do
Enum.map(params, &transform_upload_value(&1, opts))
end
defp transform_upload_value(%{__struct__: Plug.Upload} = upload, opts) do
case Majic.perform(upload.path, opts) do
{:ok, magic} -> fix_upload(upload, magic, opts)
{:error, _error} -> upload
end
end
defp transform_upload_value(%{__struct__: _} = struct, _opts) do
struct
end
defp transform_upload_value(v, opts) when is_map(v) do
transform_uploads(v, opts)
end
defp transform_upload_value(v, opts) when is_list(v) do
transform_uploads(v, opts)
end
defp transform_upload_value(v, _opts) do
v
end
defp fix_upload(upload, magic, opts) do
filename =
if Keyword.get(opts, :fix_extension) do
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
%{upload | content_type: magic.mime_type, filename: filename || upload.filename}
end
end
end
diff --git a/test/majic/majic_test.exs b/test/majic/majic_test.exs
index ba6a9f2..42efc59 100644
--- a/test/majic/majic_test.exs
+++ b/test/majic/majic_test.exs
@@ -1,97 +1,107 @@
defmodule MajicTest do
use Majic.MagicCase
alias Majic.Result
doctest Majic
@iterations 100
test "Makefile is text file" do
{:ok, pid} = Majic.Server.start_link([])
path = absolute_path("Makefile")
assert {:ok, %{mime_type: "text/x-makefile"}} = Majic.Server.perform(pid, path)
end
test "With Majic.perform" do
{:ok, pid} = Majic.Server.start_link([])
path = absolute_path("Makefile")
assert {:ok, %{mime_type: "text/x-makefile"}} = Majic.perform(path, server: pid)
end
@tag external: true
test "Load test local files" do
{:ok, pid} = Majic.Server.start_link([])
files_stream()
|> Stream.cycle()
|> Stream.take(@iterations)
|> Stream.map(&assert {:ok, %Result{}} = Majic.Server.perform(pid, &1))
|> Enum.all?()
|> assert
end
test "Non-existent file" do
{:ok, pid} = Majic.Server.start_link([])
path = missing_filename()
assert_no_file(Majic.Server.perform(pid, path))
end
test "Bytes" do
{:ok, pid} = Majic.Server.start_link([])
bytes = File.read!("test/fixtures/cat.webp")
assert {:ok, _} = Majic.Server.perform(pid, {:bytes, bytes})
end
test "Named process" do
{:ok, pid} = Majic.Server.start_link(name: :gen_magic)
path = absolute_path("Makefile")
assert {:ok, %{cycles: 0}} = Majic.Server.status(:gen_magic)
assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
assert {:ok, %Result{} = result} = Majic.Server.perform(:gen_magic, path)
assert {:ok, %{cycles: 1}} = Majic.Server.status(:gen_magic)
assert {:ok, %{cycles: 1}} = Majic.Server.status(pid)
assert "text/x-makefile" = result.mime_type
end
describe "custom database" do
setup do
database = compile_magic_db(absolute_path("test/elixir"))
on_exit(fn -> File.rm(database) end)
[database: database]
end
defp compile_magic_db(source) do
- target = Path.join(Path.dirname(source), Path.basename(source, Path.extname(source)) <> ".mgc")
+ target =
+ Path.join(Path.dirname(source), Path.basename(source, Path.extname(source)) <> ".mgc")
+
file_cmd = find_file_cmd()
- case System.cmd(file_cmd, ["-C", "-m", source], cd: Path.dirname(source), stderr_to_stdout: true) do
+ case System.cmd(file_cmd, ["-C", "-m", source],
+ cd: Path.dirname(source),
+ stderr_to_stdout: true
+ ) do
{_, 0} -> :ok
{output, _} -> raise "file -C failed: #{output}"
end
target
end
defp find_file_cmd do
- candidates = ["/opt/homebrew/opt/file-formula/bin/file", "/usr/local/opt/file-formula/bin/file", "file"]
+ candidates = [
+ "/opt/homebrew/opt/file-formula/bin/file",
+ "/usr/local/opt/file-formula/bin/file",
+ "file"
+ ]
+
Enum.find(candidates, &File.exists?/1) || "file"
end
test "recognises Elixir files", %{database: database} do
{:ok, pid} = Majic.Server.start_link(database_patterns: [database])
path = absolute_path("mix.exs")
assert {:ok, %Result{} = result} = Majic.Server.perform(pid, path)
assert "text/x-elixir" = result.mime_type
assert "us-ascii" = result.encoding
assert "Elixir module source text" = result.content
end
test "recognises Elixir files after a reload", %{database: database} do
{:ok, pid} = Majic.Server.start_link([])
path = absolute_path("mix.exs")
{:ok, %Result{mime_type: mime}} = Majic.Server.perform(pid, path)
refute mime == "text/x-elixir"
:ok = Majic.Server.reload(pid, [database])
assert {:ok, %Result{mime_type: "text/x-elixir"}} = Majic.Server.perform(pid, path)
end
end
end
diff --git a/test/majic/plug_test.exs b/test/majic/plug_test.exs
index 51fa47b..2e3fed1 100644
--- a/test/majic/plug_test.exs
+++ b/test/majic/plug_test.exs
@@ -1,120 +1,127 @@
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
#{File.read!("Makefile")}\r
------w58EW1cEpjzydSCq\r
Content-Disposition: form-data; name=\"form[make][file]\"; filename*=\"utf-8''mymakefile.txt\"\r
Content-Type: text/plain\r
\r
#{File.read!("Makefile")}\r
------w58EW1cEpjzydSCq\r
Content-Disposition: form-data; name=\"cat\"; filename*=\"utf-8''cute-cat.jpg\"\r
Content-Type: image/jpg\r
\r
#{File.read!("test/fixtures/cat.webp")}\r
------w58EW1cEpjzydSCq\r
Content-Disposition: form-data; name=\"cats[]\"; filename*=\"utf-8''first-cute-cat.jpg\"\r
Content-Type: image/jpg\r
\r
#{File.read!("test/fixtures/cat.webp")}\r
------w58EW1cEpjzydSCq\r
Content-Disposition: form-data; name=\"cats[]\"\r
\r
hello i am annoying
\r
------w58EW1cEpjzydSCq\r
Content-Disposition: form-data; name=\"cats[]\"; filename*=\"utf-8''second-cute-cat.jpg\"\r
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
"""
orig_conn =
conn(:post, "/", multipart)
|> put_req_header("content-type", "multipart/mixed; boundary=----w58EW1cEpjzydSCq")
|> TestRouter.call(@router_opts)
plug = Majic.Plug.init(once: true, fix_extension: true, startup_timeout: 5000)
plug_no_ext = Majic.Plug.init(once: true, fix_extension: false, startup_timeout: 5000)
- plug_append_ext = Majic.Plug.init(once: true, fix_extension: true, append: true, startup_timeout: 5000)
+
+ plug_append_ext =
+ Majic.Plug.init(once: true, fix_extension: true, append: true, startup_timeout: 5000)
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
assert get_in(conn.body_params, ["form", "makefile"]) ==
get_in(conn.params, ["form", "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_no_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"])
assert get_in(conn.params, ["form", "make", "file"]).content_type == "text/x-makefile"
assert get_in(conn.body_params, ["cat"]) == get_in(conn.params, ["cat"])
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"
assert Enum.all?(conn.params["cats"], fn
%Plug.Upload{} = upload -> upload.content_type == "image/webp"
%{"inception" => %{"cat" => upload}} -> upload.content_type == "image/webp"
_ -> true
end)
end
test "falls back to original upload on majic error" do
- upload = %Plug.Upload{path: "/nonexistent/path/file.txt", filename: "file.txt", content_type: "text/plain"}
+ upload = %Plug.Upload{
+ path: "/nonexistent/path/file.txt",
+ filename: "file.txt",
+ content_type: "text/plain"
+ }
+
conn = conn(:post, "/", %{"file" => upload})
plug = Majic.Plug.init(once: true, startup_timeout: 5000)
result = Majic.Plug.call(conn, plug)
assert result.params["file"].content_type == "text/plain"
assert result.params["file"].filename == "file.txt"
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sun, Aug 9, 10:46 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1722876
Default Alt Text
(17 KB)

Event Timeline