Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85627792
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Aug 8, 4:18 AM (8 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1722876
Default Alt Text
(17 KB)
Attached To
Mode
R20 majic
Attached
Detach File
Event Timeline
Log In to Comment