diff --git a/lib/waffle/actions/store.ex b/lib/waffle/actions/store.ex index 90a6ff7..2c0cdc5 100644 --- a/lib/waffle/actions/store.ex +++ b/lib/waffle/actions/store.ex @@ -68,10 +68,18 @@ defmodule Waffle.Actions.Store do defp put(_definition, {error = {:error, _msg}, _scope}), do: error defp put(definition, {%Waffle.File{} = file, scope}) do + with {:ok, file} <- validate(definition, {file, scope}), + {:ok, file} <- normalize(definition, {file, scope}) do + definition + |> put_versions({file, scope}) + |> cleanup!(file) + end + end + + defp validate(definition, {file, scope}) do case definition.validate({file, scope}) do result when result == true or result == :ok -> - put_versions(definition, {file, scope}) - |> cleanup!(file) + {:ok, file} {:error, message} -> {:error, message} @@ -81,6 +89,19 @@ defmodule Waffle.Actions.Store do end end + defp normalize(definition, {file, scope}) do + case definition.normalize({file, scope}) do + {:ok, %Waffle.File{} = file} -> + {:ok, file} + + {:error, message} -> + {:error, message} + + _ -> + {:error, :normalization_error} + end + end + defp put_versions(definition, {file, scope}) do if definition.async() do definition.__versions() @@ -158,5 +179,4 @@ defmodule Waffle.Actions.Store do result end - end diff --git a/lib/waffle/definition/storage.ex b/lib/waffle/definition/storage.ex index eac884a..f1d43e9 100644 --- a/lib/waffle/definition/storage.ex +++ b/lib/waffle/definition/storage.ex @@ -87,6 +87,27 @@ defmodule Waffle.Definition.Storage do Any other return value will return `{:error, :invalid_file}` when passed through to `Avatar.store`. + ## File Normalization + + The `normalize/1` function is used to normalize the file before processing. + The function must return either `{:ok, %Waffle.File{}}` or `{:error, message}`. + + Here is an example of generating a unique file name using the `normalize` function: + + defmodule Avatar do + use Waffle.Definition + + def normalize({file, _}) do + ext = file.file_name |> Path.extname() |> String.downcase() + new_file_name = :crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower) |> Kernel.<>(ext) + new_path = file.path |> Path.dirname() |> Path.join(new_file_name) + + with :ok <- File.rename(file.path, new_path) do + {:ok, %{file | path: new_path, file_name: new_file_name}} + end + end + end + ## Passing custom headers when downloading from remote path By default, when downloading files from remote path request headers are empty, @@ -120,6 +141,7 @@ defmodule Waffle.Definition.Storage do def storage_dir_prefix, do: Application.get_env(:waffle, :storage_dir_prefix, "") def storage_dir(_, _), do: Application.get_env(:waffle, :storage_dir, "uploads") def validate(_), do: true + def normalize({file, _scope}), do: {:ok, file} def default_url(version, _), do: default_url(version) def default_url(_), do: nil def __storage, do: Application.get_env(:waffle, :storage, Waffle.Storage.S3) @@ -128,6 +150,7 @@ defmodule Waffle.Definition.Storage do storage_dir: 2, filename: 2, validate: 1, + normalize: 1, default_url: 1, default_url: 2, __storage: 0, diff --git a/test/actions/store_test.exs b/test/actions/store_test.exs index 416276d..7f9e9ab 100644 --- a/test/actions/store_test.exs +++ b/test/actions/store_test.exs @@ -55,6 +55,27 @@ defmodule WaffleTest.Actions.Store do def __versions, do: [:original, :thumb, :skipped] end + defmodule DummyDefinitionWithNormalization do + use Waffle.Actions.Store + use Waffle.Definition.Storage + + def normalize({file, _scope}) do + ext = file.file_name |> Path.extname() |> String.downcase() + new_file_name = file.file_name |> Path.basename(ext) |> Kernel.<>("_normalized#{ext}") + new_path = storage_dir_prefix() |> Path.join(new_file_name) + + with :ok <- File.cp(file.path, new_path) do + {:ok, %{file | path: new_path, file_name: new_file_name}} + end + end + + def storage_dir_prefix, do: "waffletest" + + def transform(_, _), do: :noaction + def __versions, do: [:original] + def __storage, do: Waffle.Storage.Local + end + test "custom transformations change a file extension" do with_mock Waffle.Storage.S3, put: fn DummyDefinitionWithExtension, _, {%{file_name: "image.jpg", path: _}, nil} -> @@ -76,6 +97,10 @@ defmodule WaffleTest.Actions.Store do assert DummyDefinitionWithValidationError.store(__ENV__.file) == {:error, "invalid file type"} end + test "normalizes file" do + assert DummyDefinitionWithNormalization.store(@img) == {:ok, "image_normalized.png"} + end + test "single binary argument is interpreted as file path" do with_mock Waffle.Storage.S3, put: fn DummyDefinition, _, {%{file_name: "image.png", path: @img}, nil} ->