Majic.Extension

This commit is contained in:
Jordan Bracco 2020-06-17 14:58:32 +02:00
parent d5dbd1a0c6
commit 4983596ab2
6 changed files with 163 additions and 76 deletions

View file

@ -145,6 +145,16 @@ And then you can use it with `Majic.perform/2` with `pool: YourApp.MajicPool` op
iex(1)> Majic.perform(Path.expand("~/.bash_history"), pool: YourApp.MajicPool) iex(1)> Majic.perform(Path.expand("~/.bash_history"), pool: YourApp.MajicPool)
{:ok, %Majic.Result{mime_type: "text/plain", encoding: "us-ascii", content: "ASCII text"}} {:ok, %Majic.Result{mime_type: "text/plain", encoding: "us-ascii", content: "ASCII text"}}
``` ```
#### Fixing extensions
You may also want to fix the user-provided filename according to its detected MIME type. To do this, you can use `Majic.Extension.fix/3`:
```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"
```
#### Use with Plug.Upload #### Use with Plug.Upload
@ -152,7 +162,8 @@ If you use Plug or Phoenix, you may want to automatically verify the content typ
`Majic.Plug` is there for this. `Majic.Plug` is there for this.
Enable it by using `plug Majic.Plug, pool: YourApp.MajicPool` in your pipeline or controller. Then, every `Plug.Upload` Enable it by using `plug Majic.Plug, pool: YourApp.MajicPool` in your pipeline or controller. Then, every `Plug.Upload`
in `conn.params` is now verified. The filename is also altered with an extension matching its content-type. in `conn.params` and `conn.body_params` is now verified. The filename is also altered with an extension matching its
content-type, using `Majic.Extension`.
## Notes ## Notes

89
lib/majic/extension.ex Normal file
View file

@ -0,0 +1,89 @@
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)
exts = MIME.extensions(mime_type) ++ subtype_extension(subtype?, mime_type)
old_ext = String.downcase(Path.extname(name))
basename = Path.basename(name, old_ext)
"." <> old = old_ext
if old in exts do
Enum.join([basename, old])
else
ext = List.first(exts)
ext_list =
cond do
ext && append? -> [old, ext]
!ext -> []
ext -> [ext]
end
Enum.join([basename] ++ ext_list, ".")
end
end
defp subtype_extension(true, type) do
[_type, sub] = String.split(type, "/", parts: 2)
[sub]
end
defp subtype_extension(_, _), do: []
end

View file

@ -10,22 +10,22 @@ if Code.ensure_loaded?(Plug) do
One of the required option of `pool`, `server` or `once` must be set. One of the required option of `pool`, `server` or `once` must be set.
Additional options: Additional options:
* `fix_extension`, default true: rewrite the user provided `filename` with a valid extension for the detected content type * `fix_extension`, default true: enable use of `Majic.Extension`,
* `append_extension`, default false: append the valid extension to the previous filename, without removing the user provided extension * options for `Majic.Extension`.
To use a gen_magic pool: To use a majic pool:
``` ```
plug Majic.Plug, pool: MyApp.MajicPool plug Majic.Plug, pool: MyApp.MajicPool
``` ```
To use a single gen_magic server: To use a single majic server:
``` ```
plug Majic.Plug, server: MyApp.MajicServer plug Majic.Plug, server: MyApp.MajicServer
``` ```
To start a gen_magic process at each file (not recommended): To start a majic process at each file (not recommended):
``` ```
plug Majic.Plug, once: true plug Majic.Plug, once: true
@ -44,7 +44,8 @@ if Code.ensure_loaded?(Plug) do
opts opts
|> Keyword.put_new(:fix_extension, true) |> Keyword.put_new(:fix_extension, true)
|> Keyword.put_new(:append_extension, false) |> Keyword.put_new(:append, false)
|> Keyword.put_new(:subtype_as_extension, false)
end end
@impl Plug @impl Plug
@ -105,71 +106,18 @@ if Code.ensure_loaded?(Plug) do
end end
defp fix_upload(upload, magic, opts) do defp fix_upload(upload, magic, opts) do
%{upload | content_type: magic.mime_type} filename =
|> fix_extension(Keyword.get(opts, :fix_extension), opts) if Keyword.get(opts, :fix_extension) do
IO.puts("FIXING EXTENSION #{inspect opts}")
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 end
defp fix_extension(upload, true, opts) do %{upload | content_type: magic.mime_type, filename: filename || upload.filename}
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, [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
# No extension for type.
defp rewrite_extension(upload, old, [], opts) do
%{
upload
| filename:
rewrite_or_append_extension(
Path.basename(upload.filename, old),
old,
nil,
Keyword.get(opts, :append_extension)
)
}
end
# Append, no extension for type: keep old extension
defp rewrite_or_append_extension(basename, "." <> old, nil, true) do
basename <> "." <> old
end
# No extension for type: only keep basename
defp rewrite_or_append_extension(basename, _, nil, _) do
basename
end
# Append
defp rewrite_or_append_extension(basename, "." <> old, ext, true) do
Enum.join([basename, old, ext], ".")
end
# Rewrite
defp rewrite_or_append_extension(basename, _, ext, _) do
basename <> "." <> ext
end end
# put value at path in conn. # put value at path in conn.

View file

@ -43,8 +43,8 @@ defmodule Majic.MixProject do
defp deps do defp deps do
[ [
{:nimble_pool, "~> 0.1"}, {:nimble_pool, "~> 0.1"},
{:mime, "~> 1.0"},
{:plug, "~> 1.0", optional: true}, {:plug, "~> 1.0", optional: true},
{:mime, "~> 1.0", optional: true},
{:credo, "~> 1.4", only: [:dev, :test], runtime: false}, {:credo, "~> 1.4", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false}, {:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false},

View file

@ -0,0 +1,33 @@
defmodule Majic.ExtensionTest do
use ExUnit.Case
alias Majic.Extension
test "it fixes extensions" do
assert "Makefile" == Extension.fix("Makefile.txt", "text/x-makefile")
assert "cat.webp" == Extension.fix("cat.jpeg", "image/webp")
end
test "it appends extensions" do
assert "Makefile" == Extension.fix("Makefile.txt", "text/x-makefile", append: true)
assert "cat.jpeg.webp" == Extension.fix("cat.jpeg", "image/webp", append: true)
end
test "it uses subtype as extension" do
assert "Makefile.x-makefile" ==
Extension.fix("Makefile.txt", "text/x-makefile", subtype_as_extension: true)
assert "cat.webp" == Extension.fix("cat.jpeg", "image/webp", subtype_as_extension: true)
end
test "it appends and use subtype" do
assert "Makefile.txt.x-makefile" ==
Extension.fix("Makefile.txt", "text/x-makefile",
subtype_as_extension: true,
append: true
)
assert "cat.jpeg.webp" ==
Extension.fix("cat.jpeg", "image/webp", subtype_as_extension: true, append: true)
end
end

View file

@ -59,6 +59,11 @@ defmodule Majic.PlugTest do
Content-Type: image/jpg\r Content-Type: image/jpg\r
\r \r
#{File.read!("test/fixtures/cat.webp")}\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 ------w58EW1cEpjzydSCq--\r
""" """
@ -69,7 +74,7 @@ defmodule Majic.PlugTest do
plug = Majic.Plug.init(once: true) plug = Majic.Plug.init(once: true)
plug_no_ext = Majic.Plug.init(once: true, fix_extension: false) 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) plug_append_ext = Majic.Plug.init(once: true, fix_extension: true, append: true)
conn = Majic.Plug.call(orig_conn, plug) conn = Majic.Plug.call(orig_conn, plug)
conn_no_ext = Majic.Plug.call(orig_conn, plug_no_ext) conn_no_ext = Majic.Plug.call(orig_conn, plug_no_ext)
@ -84,7 +89,7 @@ defmodule Majic.PlugTest do
assert get_in(conn.params, ["form", "makefile"]).content_type == "text/x-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.params, ["form", "makefile"]).filename == "mymakefile"
assert get_in(conn_no_ext.params, ["form", "makefile"]).filename == "mymakefile.txt" assert get_in(conn_no_ext.params, ["form", "makefile"]).filename == "mymakefile.txt"
assert get_in(conn_append_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"]) == assert get_in(conn.body_params, ["form", "make", "file"]) ==
get_in(conn.params, ["form", "make", "file"]) get_in(conn.params, ["form", "make", "file"])
@ -99,6 +104,7 @@ defmodule Majic.PlugTest do
assert Enum.all?(conn.params["cats"], fn assert Enum.all?(conn.params["cats"], fn
%Plug.Upload{} = upload -> upload.content_type == "image/webp" %Plug.Upload{} = upload -> upload.content_type == "image/webp"
%{"inception" => %{"cat" => upload}} -> upload.content_type == "image/webp"
_ -> true _ -> true
end) end)
end end