Packages

File upload library for Elixir

Current section

Files

Jump to
uploader lib ecto changeset.ex
Raw

lib/ecto/changeset.ex

defmodule Uploader.Ecto.Changeset do
import Ecto.Changeset
@doc ~S"""
Returns a changeset for the given `struct` (see `Ecto.Changeset.cast/4`).
Casts file uploads (`Plug.Upload` structs) into a generated UUID filename or, if the
`:cast` option is present, into a value obtained by a call to the user-defined
cast function.
In order not to lose the `Plug.Upload` structs after casting, these structs are
stored into virtual fields named by the original fields that held the file uploads
prefixed by "uploaded_".
Example:
* A param named "image" contains a file upload (`Plug.Upload` struct);
* `cast_with_upload/3` casts the `Plug.Upload` struct into a filename;
* the changeset field :image contains the filename;
* the changeset field :uploaded_image contains the `Plug.Upload` struct.
"""
def cast_with_upload(%{__struct__: _} = struct, params, permitted) do
uploadable_fields_with_opts = struct.__struct__.get_uploadable_fields()
uploadable_fields = Keyword.keys(uploadable_fields_with_opts)
# Move the file uploads—Plug.Upload structs—into the virtual fields
# as we do not want to lose these structs after casting. Uploading the
# files takes place later on and we need the data from the Plug.Upload
# structs.
params_with_virtual_fields =
Enum.map(params, fn
{k, %Plug.Upload{} = v} -> {"uploaded_" <> k, v}
{k, v} -> {k, v}
end)
|> Enum.into(%{})
# List of virtual fields containing the file uploads.
uploadable_virtual_fields =
uploadable_fields |> Enum.map(&String.to_atom("uploaded_" <> Atom.to_string(&1)))
# Create a changeset with the Plug.Upload structs. The reason an intermediary
# changeset is created here (we still did not cast the Plug.Upload structs
# into filenames) is because we want to pass this changeset to the user-defined
# cast function. The changeset is passed because the filename might be deduced
# from a field from the Schema.
permitted = (permitted -- uploadable_fields) ++ uploadable_virtual_fields
changeset = cast(struct, params_with_virtual_fields, permitted)
# Cast the Plug.Upload structs.
casted_upload_params =
Enum.filter(params, fn
{_, %Plug.Upload{}} -> true
_ -> false
end)
|> Enum.map(fn {k, %Plug.Upload{} = upload} ->
uploadable_field_opts = Keyword.fetch!(uploadable_fields_with_opts, String.to_atom(k))
fn_cast_upload = Keyword.get(uploadable_field_opts, :cast, &cast_upload/2)
{k, fn_cast_upload.(upload, changeset)}
end)
|> Enum.into(%{})
# Create a changeset containing the casted Plug.Upload structs (a Plug.Upload struct
# will typically be casted into a filename.
changeset_filenames = cast(struct, casted_upload_params, uploadable_fields)
Ecto.Changeset.merge(changeset, changeset_filenames)
end
defp cast_upload(
%Plug.Upload{filename: uploaded_filename},
%Ecto.Changeset{}
) do
filename = Ecto.UUID.generate()
extension = Path.extname(uploaded_filename) |> String.downcase()
filename <> extension
end
end