How to upload a file in Elixir with Waffle

The ability to upload files is a key requirement for many todays web and mobile applications. In this tutorial, we will look at how we can accomplish file uploads to local storage and S3 server in Phoenix with the help of Waffle library.

Table of contents

    Background

    Waffle is a flexible file upload library for Elixir with straightforward integrations for Amazon S3 and ImageMagick. This library is forked from Arc and works much in the same way. To illustrate how to upload files we will start with a simple demo application, which let us upload pictures to local storage and viewing pictures on page. We will also use Waffle.Ecto library to integrate Waffle with Ecto and save file data in the database. At the end, we will check how to upload files to S3 server.

    Demo app

    It's time to create our app:

    mix phx.new file_uploader

    Let's take a few shortcuts and create everything that we need with one command in the project folder:

    mix phx.gen.html Gallery Photo photos picture:string

    The above command mix phx.gen.html generates controller, views, and context for an HTML resource. We will have a table named "photos" in the database with id, picture (for storing data about the file) and timestamps columns.

    We also need to provide an appropriate route for our new controller in router.ex so update the existing scope just like below:

    scope "/", FileUploaderWeb do
      pipe_through :browser
      get "/", PageController, :index
    
      resources "/photos", PhotoController
    end

    Now provide your database credentials in your config and run

    mix ecto.create && mix ecto.migrate

    to set up a database. You can run mix phx.server to start an application and go to http://localhost:4000/photos just to take a look at what we have created at the moment. In the next chapter, we are going to add Waffle and Waffle.Ecto to our project.

    Setup Waffle

    Add the latest stable release of Waffle and Waffle.Ecto to your mix.exs file:

    defp deps do
      [
        ...
        {:waffle,  "~> 1.1.5"},
        {:waffle_ecto, "~> 0.0.11"}
        ...
      ]
    end

    and after that:

    mix deps.get

    Next, we should generate a definition module with mix waffle.g :

     mix waffle.g file_image

    With above command we generated file in lib/[APP_NAME]_web/uploaders/file_image.ex. For now, everything in this file is commented out but we will provide some changes there in next section.

    Local Storage

    To store files locally in our project file system we will start with the setup of the storage provider in dev config:

    config :waffle, storage: Waffle.Storage.Local

    Now let's provide some changes to lib/file_uploader_web/uploaders/file_image.ex. Add use Waffle.Ecto.Definition at the top of the file which includes ecto support. We can also provide some file validation to our application. Let's assume we want uploaded files to have the following extensions: .jpg .jpeg .gif .png so let's add validation function to file_image.ex:

    defmodule FileUploader.FileImage do
      use Waffle.Definition
      use Waffle.Ecto.Definition
    
      # Whitelist file extensions:
      def  validate({file, _}) do
        file_extension = file.file_name |> Path.extname() |> String.downcase()
        case  Enum.member?(~w(.jpg .jpeg .gif .png), file_extension) do
          true -> :ok
          false -> {:error, "invalid file type"}
        end
      end
    end

    Next part is to add the uploader to the Photo module in lib/file_uploader/gallery/photo.ex:

    defmodule FileUploader.Gallery.Photo do
      use  Ecto.Schema
      use  Waffle.Ecto.Schema
      import  Ecto.Changeset
    
      schema "photos" do
        field :picture, FileUploader.FileImage.Type
    
        timestamps()
      end
    
      @doc false
      def changeset(photo, attrs) do
        photo
        |> cast_attachments(attrs, [:picture])
        |> validate_required([:picture])
      end
    end

    By default, our files are saved in /uploads folder. So now we need to configure the endpoint.ex to indicate that we will be serving static resources from the /uploads directory. Go to the lib/file_uploader_web/endpoint.ex and add a second static plug:

    plug Plug.Static, at: "/uploads", from: Path.expand('./uploads'), gzip: false

    We managed to configure everything on the backend side. Now we need to add some changes to the templates we generated earlier. We will start from lib/file_uploader_web/templates/photo/index.html.eex. We should change: <td><%= photo.picture %></td> to:

    <td>
      <%= img_tag FileUploader.FileImage.url({photo.picture, photo}, signed: true)
      %>
    </td>

    The same change we need to provide in our show template in lib/file_uploader_web/templates/photo/show.html.eex. Let's change

    <%= @photo.picture %>

    to:

    <%= img_tag FileUploader.FileImage.url({@photo.picture, @photo}, signed: true) %>

    This change will allow us to display the photos correctly. We use the url/4 function which is injected into our FileImage module through use Waffle.Definition macro.

    Okay, so now we can display our pictures, but we also need to make some changes when it comes to adding new images. Let's go to our form.html.eex template in lib/file_uploader_web/templates/photo/form.html.eex and change text input

    <%= text_input f, :picture %>

    to file input:

    <%= file_input f, :picture %>

    We also need to change our form into a multipart form. The form_for/4 function accepts a keyword list of options where we can specify this:

    <%= form_for @changeset, @action, [multipart: true], fn f -> %>

    Now let's check our changes and how what we've done works. You can run your local server and go to http://localhost:4000/photos, try to add a new photo and check if everything works fine.

    Upload files to S3

    If we want to upload files to Amazon S3 we need to add some extra libraries from ExAws to what we already have:

    defp deps do
      [
        {:waffle, "~> 1.1.0"},
    
        # If using S3:
        {:ex_aws, "~> 2.1.2"},
        {:ex_aws_s3, "~> 2.0"},
        {:hackney, "~> 1.9"},
        {:sweet_xml, "~> 0.6"}
      ]
    end

    After that, it's time to set up some configurations. In this case, we are going to use Waffle.Storage.S3:

    config :waffle,
      storage: Waffle.Storage.S3,
      bucket: "your_bucket"

    and we must add ex_aws configuration:

    config :ex_aws,
      json_codec: Jason
      # any configurations provided by https://github.com/ex-aws/ex_aws
    Download our ebook
    Mateusz Tatarski
    Mateusz Tatarski Elixir Developer

    Read more
    on #curiosum blog

    Top 5 Elixir Skills to Learn in 2021 [for Juniors]

    Elixir is not magic - despite being easy to write in and learn, you need a strong foundation. In this short article, I will give you my personal list of things that are necessary (or at least very much a “should-have”) for Junior Elixir developers.

    Debugging Elixir Code: The Definitive Guide

    Every application contains bugs, and even if it doesn't, it will. And even if you're not a notorious bug producer and your code quality is generally good, the conditions programmers work in are often suboptimal - you will often find yourself pulling your hair out over code written years ago by someone no longer involved in your project.