diff --git a/lib/waffle/definition/validation.ex b/lib/waffle/definition/validation.ex new file mode 100644 index 0000000..9ee91c5 --- /dev/null +++ b/lib/waffle/definition/validation.ex @@ -0,0 +1,78 @@ +defmodule Waffle.Definition.Validation do + @moduledoc """ + File validation by MIME types + + Validation.validate("mix.exs", ["text/x-ruby"]) + => :ok + + Validation process: + + 1. We get content type of a file by the `file` utility + + 2. We check that `MIME` library recognizes such content type + + 3. Next, we check that returnted content type matches expected + extensions list for that particular content type + + 4. Finally, we check if this content type is allowed + + """ + + def validate(filepath, :all), do: :ok + + def validate(filepath, valid_content_types) do + with {:ok, content_type} <- content_type(filepath), + :ok <- mime_is_valid(content_type), + :ok <- extension_matches_mime(filepath, content_type), + :ok <- mime_is_allowed(valid_content_types, content_type) do + :ok + else + {:error, message} -> {:error, message} + end + end + + def mime_is_valid(content_type) do + if MIME.valid?(content_type) do + :ok + else + {:error, ["content type is invalid"]} + end + end + + def extension_matches_mime(filepath, content_type) do + # TODO add custom extensions + if MIME.extensions(content_type) + |> Enum.member?(filepath |> Path.extname() |> String.downcase()) do + :ok + else + {:error, ["content type and extension doesn't match"]} + end + end + + def mime_is_allowed(valid_content_types, content_type) do + if Enum.member?(valid_content_types, content_type) do + :ok + else + {:error, ["invalid file format"]} + end + end + + def content_type(filepath) do + with true <- File.exists?(filepath), + {file_utility_output, 0} <- System.cmd("file", ["--mime", "--brief", filepath]) do + content_type = + Regex.named_captures( + ~r/^(?.+);/, + file_utility_output + )["content_type"] + + {:ok, content_type} + else + {error, 1} -> + {:error, error} + + false -> + "inode/x-empty" + end + end +end diff --git a/mix.exs b/mix.exs index 796f26e..07cb7fd 100644 --- a/mix.exs +++ b/mix.exs @@ -54,6 +54,9 @@ defmodule Waffle.Mixfile do [ {:hackney, "~> 1.9"}, + # file validation + {:mime, "~> 1.2"}, + # If using Amazon S3 {:ex_aws, "~> 2.1.2", optional: true}, {:ex_aws_s3, "~> 2.0", optional: true}, diff --git a/mix.lock b/mix.lock index 93c8717..dd1d086 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm"},