diff --git a/README.md b/README.md index 57598af..5c574cc 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,32 @@ Both public and signed urls will include the timestamp for cache busting, and ar MyApp.Avatar.url({user.avatar, user}, :thumb, signed: true) ``` +### Delete attached file from storage + +Attachments can be deleted using `delete_attachments/2` before calling `Repo.delete/1`. + +```elixir +defmodule MyApp.User do + # ... + + def delete_changeset(data) do + data + |> changeset(%{}) + |> delete_attachments(@attachments) + end +end +``` + +Then, to delete, instead of calling `Repo.delete` from element, do it from deletion +changeset (like when using `Ecto.Changeset.prepare_changes/2`): + +```elixir + user + |> MyApp.User.delete_changeset() + |> MyApp.Repo.delete() + +``` + ## License Copyright 2015 Sean Stavropoulos diff --git a/lib/arc_ecto/definition.ex b/lib/arc_ecto/definition.ex index ee1c149..08b724a 100644 --- a/lib/arc_ecto/definition.ex +++ b/lib/arc_ecto/definition.ex @@ -6,6 +6,7 @@ defmodule Arc.Ecto.Definition do defmodule Module.concat(unquote(definition), "Type") do @behaviour Ecto.Type def type, do: Arc.Ecto.Type.type + def definition, do: unquote(definition) def cast(value), do: Arc.Ecto.Type.cast(unquote(definition), value) def load(value), do: Arc.Ecto.Type.load(unquote(definition), value) def dump(value), do: Arc.Ecto.Type.dump(unquote(definition), value) @@ -30,7 +31,7 @@ defmodule Arc.Ecto.Definition do end def url(f, v, options), do: super(f, v, options) - + def delete({%{file_name: file_name, updated_at: _updated_at}, scope}), do: super({file_name, scope}) def delete(args), do: super(args) diff --git a/lib/arc_ecto/schema.ex b/lib/arc_ecto/schema.ex index 3b9a56c..e924cd6 100644 --- a/lib/arc_ecto/schema.ex +++ b/lib/arc_ecto/schema.ex @@ -2,6 +2,26 @@ defmodule Arc.Ecto.Schema do defmacro __using__(_) do quote do import Arc.Ecto.Schema + + def delete_attachments(changeset_or_data, fields) do + scope = case changeset_or_data do + %Ecto.Changeset{} -> Ecto.Changeset.apply_changes(changeset_or_data) + %{__meta__: _} -> changeset_or_data + end + Enum.each(fields, &(delete_attachment(scope, &1))) + changeset_or_data + end + + defp delete_attachment(scope, field) do + type = __MODULE__.__schema__(:type, field) + definition = type.definition() + value = Map.get(scope, field) + if value do + {value, scope} + |> definition.urls() + |> Enum.each(fn {_size, path} -> definition.delete({path, scope}) end) + end + end end end diff --git a/mix.exs b/mix.exs index 45484d7..91a07e7 100644 --- a/mix.exs +++ b/mix.exs @@ -7,6 +7,7 @@ defmodule Arc.Ecto.Mixfile do [app: :arc_ecto, version: @version, elixir: "~> 1.0", + elixirc_paths: elixirc_paths(Mix.env), deps: deps(), # Hex @@ -21,6 +22,10 @@ defmodule Arc.Ecto.Mixfile do [applications: [:logger, :arc]] end + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + defp description do """ An integration with Arc and Ecto. diff --git a/test/schema_test.exs b/test/schema_test.exs index 86f71a1..d7e25c2 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -2,10 +2,7 @@ defmodule ArcTest.Ecto.Schema do use ExUnit.Case, async: false import Mock - defmodule DummyDefinition do - use Arc.Definition - use Arc.Ecto.Definition - end + alias ArcTest.Ecto.DummyDefinition defmodule TestUser do use Ecto.Schema @@ -36,15 +33,12 @@ defmodule ArcTest.Ecto.Schema do |> cast(params, ~w(first_name)a) |> cast_attachments(params, ~w(avatar)a) end - end - setup do - defmodule DummyDefinition do - use Arc.Definition - use Arc.Ecto.Definition + def delete_changeset(user, params) do + user + |> changeset(params) + |> delete_attachments(~w(avatar)a) end - - :ok end def build_upload(path) do @@ -70,7 +64,7 @@ defmodule ArcTest.Ecto.Schema do cs = TestUser.changeset(%TestUser{}, %{"avatar" => upload}) assert called DummyDefinition.store({upload, %TestUser{}}) assert cs.valid? == false - assert cs.errors == [avatar: {"is invalid", [type: ArcTest.Ecto.Schema.DummyDefinition.Type]}] + assert cs.errors == [avatar: {"is invalid", [type: DummyDefinition.Type]}] end test_with_mock "converts changeset into schema", DummyDefinition, [store: fn({%{__struct__: Plug.Upload, path: "/path/to/my/file.png", file_name: "file.png"}, %TestUser{}}) -> {:error, :invalid_file} end] do @@ -101,4 +95,12 @@ defmodule ArcTest.Ecto.Schema do changeset = TestUser.path_changeset(%TestUser{}, %{"avatar" => "/path/to/my/file.png"}) assert called DummyDefinition.store({"/path/to/my/file.png", %TestUser{}}) end + + test_with_mock "deletes attachments", DummyDefinition, [store: fn({%{__struct__: Plug.Upload, path: "/path/to/my/file.png", file_name: "file.png"}, %TestUser{}}) -> {:ok, "file.png"} end, + urls: fn({%{file_name: "file.png"}, %TestUser{}}) -> %{original: "file.png"} end, + delete: fn({"file.png", %TestUser{}}) -> :ok end] do + upload = build_upload("/path/to/my/file.png") + TestUser.delete_changeset(%TestUser{}, %{"avatar" => upload}) + assert called DummyDefinition.delete({"file.png", :_}) + end end diff --git a/test/support/dummy_definition.ex b/test/support/dummy_definition.ex new file mode 100644 index 0000000..089230e --- /dev/null +++ b/test/support/dummy_definition.ex @@ -0,0 +1,4 @@ +defmodule ArcTest.Ecto.DummyDefinition do + use Arc.Definition + use Arc.Ecto.Definition +end