Uploader
Uploader
helps you with storing file uploads.
There are numerous file upload libraries for Elixir already available, however
this library was made in order to have a finer control over how Plug.Upload
are casted into schema fields, over the filenames that should be generated,
the strategy to apply for existing file paths, etc.
Imagine a single image upload that must to be stored multiple times with filenames in different languages for SEO purposes. The writing of this library was motivated to address such advanced use cases.
Define the Schema fields holding uploads
defmodule MyApp.User do
use Ecto.Schema
use Uploader.Ecto.UploadableFields
schema "posts" do
uploadable_field :avatar_image
end
end
You may import :uploader
's formatter configuration by importing
uploader
into your .formatter.exs
file (this allows for example to keep
uploadable_field :avatar_image
without parentheses when running mix format
).
[
import_deps: [:ecto, :phoenix, :uploader],
#...
]
Cast Plug.Upload
structs
In order to cast file uploads (Plug.Upload
structs) into filenames, you must
call cast_with_upload/3
from the Uploader.Ecto.Changeset
module for the
given Schema struct and params. Note that, in addition to casting file uploads,
cast_with_upload/3
implicitly calls Ecto.Changeset.cast/4
for the given params
and returns the changeset.
import Uploader.Ecto.Changeset
schema "user" do
uploadable_field :avatar_image
end
@required_fields ~w(avatar_image)a
@optional_fields ~w()a
def changeset(user, attrs) do
user
|> cast_with_upload(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
end
Copy the uploaded files
This library expects the storing of files to happen in a transaction. This is done
with a call to Uploader.store_files/1
given a Schema struct.
Multi.new()
|> Multi.insert(:user, user_changeset)
|> Multi.run(:upload_files, fn _repo, %{user: user} ->
Uploader.store_files(user)
end)
|> Repo.transaction()
|> case do
{:ok, %{user: user}} ->
{:ok, user}
{:error, :user, %Ecto.Changeset{} = changeset, _changes} ->
{:error, changeset}
{:error, :upload_files, {:file_path_exists, file_path}, _changes} ->
raise "file upload failed: path \"#{file_path}\" already exists"
end
Print URLs to uploaded files
A view helper is provided in order to print the URL of uploaded files.
Open up the entrypoint defining your web interface, such as MyAppWeb
, and
add the line below into the view
function's quote
block.
def view do
quote do
# some code
import Uploader.HTML.UploadHelpers
end
end
The helper below prints the URL for an uploaded file:
<%= upload_url(user, :avatar_image) %>
You may also prepend the URL with a base URL:
<%= upload_url(user, :avatar_image, base_url: upload_path()) %>
Field options
The uploadable_field/2
macro may optionally receive options to alter the way
Plug.Upload
structs are casted, how filenames are generated, etc. Here is a
list of options:
:cast
: a function that casts aPlug.Upload
struct into the value to be stored in the database.directory
: the base directory containing the file uploads.filename
: a function that generates the filename based on the given struct.on_file_exists
: specifies the strategy to apply if the file path already exists. Its value may be::overwrite
: overwrite the file if the file path already exists:compare_hash
: do not copy the file if the file path already exists; if the hashes of the files' content are not equal, return an error.
print
: a function that prints the field (typically used be the view).type
: the field type.
Example case for custom options
A CMS allows the upload of a blog post's cover image. The blog post is available in multiple languages and the cover image must be stored multiple times in different languages for SEO purposes.
For example, the cover image for my post about my favorite books must have the name "my-favorite-books.jpg" in English, "mes-livres-preferes.jpg" in French, etc. We should then store all the images' filenames and retrieve the right filename according to the language used by the reader.
The image upload (Plug.Upload
) must be casted into a map of filenames in
different languages:
%{
"en" => "my-favorite-books.jpg",
"fr" => "mes-livres-preferes.jpg",
"nl" => "mijn-lievelingsboeken.jpg"
}
We want those filenames to have the same name as the post's URL slug (which are
held by another Schema field :slug
) with the file extension appended. I.e. if
the English URL slug for that post is my-favorite-books
, the cover image will
be named my-favorite-books.jpg
in English.
Below are the Schema fields with the custom options to make that work.
The code below uses the translatable_field/1
macro from the i18n_helpers
package. A translatable field holds a map of values for different languages
as the map shown above, and generates a virtual field (field prepended by
"translated_") holding the translated field. For example the :slug
field
may contain a map such as %{"en" => "favorite-books", "fr" => "livres-preferes"}
and :translated_slug
will contain livres-preferes
if the struct has been
translated in French. See i18n_helpers
's readme.
translatable_field :slug
translatable_field :cover_image
uploadable_field :cover_image,
cast: &User.cast_cover_image/2,
directory: @uploads_directory,
filename: &User.filename_cover_image/2,
on_file_exists: :compare_hash,
print: &User.print_cover_image/2,
type: :map
# Cast the Plug.Upload struct into a map of filenames per language.
def cast_cover_image(
%Plug.Upload{filename: uploaded_filename},
%Ecto.Changeset{} = changeset
) do
slugs = fetch_field!(changeset, :slug)
extension = Path.extname(uploaded_filename) |> String.downcase()
Enum.map(slugs, fn {language, slug} -> {language, slug <> extension} end) |> Enum.into(%{})
end
# Return the list of filenames to be stored.
def filename_cover_image(%User{cover_image: cover_image}, _field_name) do
Map.values(cover_image)
end
# Return the filename to be printed.
def print_cover_image(%User{translated_cover_image: translated_cover_image}, _field_name) do
translated_cover_image
end
Installation
Add uploader
for Elixir as a dependency in your mix.exs
file:
def deps do
[
{:uploader, "~> 0.1.0"}
]
end
HexDocs
HexDocs documentation can be found at https://hexdocs.pm/uploader.