Let's imagine that our Product Owner decided it was absolutely essential for our customers to be able to attach some images to their stored data. You need it secure and done ASAP (last week), this is when you should have some interests in using the Arc library. As described on GitHub, it is a flexible file upload and attachment library for Elixir.
It allows you to simply manipulate and store (using separate threads of course) images on various providers like Amazon S3. Actually, it do not only manage images but also any binary file upload.
I needed to upload images and word / PDF documents to my data model to store them on S3 and link it in my datastore to my model.
I’ll let you have a look at the official Arc documentation to grab all the details but in my case I chose to store files at the path /uploads/data/{data.id}/medias/{version_filename} were data.id is the data UUID, version the thumbed or original version and filename the original file name.
defmodule MyApp.MediaUploader do use Arc.Definition use Arc.Ecto.Definition alias MyApp.Repo # To add a thumbnail version: @versions [:original, :thumb] # Whitelist file extensions: def validate({file, _}) do ~w(.jpg .jpeg .gif .png .pdf) |> Enum.member?(Path.extname(file.file_name)) end # Define a thumbnail transformation: def transform(:thumb, {file, _scope}) do if Enum.member?(~w(.jpg .jpeg .gif .png), Path.extname(file.file_name)) do {:convert, "-strip -thumbnail 250x250^ -format png", :png} else :noaction end end # Override the persisted filenames: def filename(version, {file, _scope}) do file_name = Path.basename(file.file_name, Path.extname(file.file_name)) "#{version}_#{file_name}" end def filename(version, _) do version end # Override the storage directory: def storage_dir(_version, {_file, media}) do "uploads/data/#{media.data.id}/medias/" end end
Let’s have a look at the model we defined using the arc_ecto library which provides a way to store the media reference in datastore.
defmodule MyApp.Media do use Ecto.Schema use Arc.Ecto.Schema import Ecto.Changeset alias MyApp.Media @primary_key {:id, Ecto.UUID, autogenerate: true} @derive {Phoenix.Param, key: :id} schema "medias" do field(:name, MyApp.MediaUploader.Type) belongs_to( :data, MyApp.Data, on_replace: :update, foreign_key: :data_id, type: Ecto.UUID ) timestamps() end @doc false def changeset(%Media{} = model, attrs \\ %{}) do model |> cast_attachments(attrs, [:name]) |> validate_required([:name]) |> foreign_key_constraint(:data_id) |> cast_assoc(:data) end end
We can see the usage of Arc.Ecto.Schema in the name field referring to our MediaUploader presented just before. It will automatically store for you the references to the media in the version you need. To get the media you’ll simply use the MediaUploader.url/1 or MediaUploader.url/2 to get the URL for a specified version.
To get back to the MediaUploader where all the logic is done, we see that the validate/1 function only allows some file extensions. The transform/2 function creates a thumbnail for images only. The storage_dir/2 and filename/2 allows us to customize the directory where we’ll store our file.
Note that when getting the media URL you can ask Arc to get a signed URL which will be available for a short time. This allows you to protect accesses to your resources like with direct access links.
I strongly encourage you to check the Arc module (hex) documentation to fully understand what it is possible to do with this awesome piece of software.
Hope you enjoy this blog post series and that you'll share it. Thanks.