Playing With Fire

Exploring the web one Elixir at a time

Uploading images to Heroku using Phoenix

Heroku is a natural platform to deploy Phoenix applications to. Heroku does have a small issue in that it does not allow any files to be uploaded out of the box. If you need that capability, then the uploaded file would need to be hosted on some other provider, like AWS or DigitalOcean - on Heroku there are plugins for that, or stored in a database. This post deals with hosting an uploaded image file in your application database and serving it for use in your Phoenix application.

 

Heroku

For those that don’t know what Heroku is, simply put its a PaaS (Platform as a Service). Details about it can be found at the Heroku website. Code is deployed using Git. To understand how to deploy, read my article on just that on this site: Hosting Phoenix Applications on Heroku, or see the excellent article on Phoenix Guides here.

 

Setting the scene.

Picture this. You’re building a Phoenix web app and you have a requirement to allow users of the site to upload images. Easy enough you say. Then you’re told that the deployment platform is Heroku and for this version the client isn’t interested in obtaining any AWS space for you to push the images to. (Yes, this really happened).

 

Flashback to “the good ol’ days”™

On of my previous roles was to maintain a high-volume sports betting site that pre-cached images into its database and with a combination of a simple API, headers and the image tag managed to spit them out again as required. That site was built in an early version of PHP, 4 I think - the Wild West of web building if ever there was one.

To solve the above scenario a technique similar to that was adopted.

 

Preparing the DB and Phoenix

I’m going to assume that you’ve already got an application started. There are plenty of resources on Phoenix, including Phoenix - Building a Chat Server in 15 minutes on this site.

So we’ll jump straight and generate a migration to handle it.

mix ecto.gen.migration add_image_uploads

This will generate a file in the standard location for migrations priv/repo/migrations

Find the generated file - your command line will tell you exactly where is it and what its called and then add in the change() function:

def change do
    create table(:images) do
        add :image_data, :binary
        add :image_name, :string
        add :image_type, :string, size: 20
    end
end

There is obviously more data to be had from the image, but this will suffice for our needs.

In fact Plug.Upload passes us the following struct…

%Plug.Upload{content_type: "<mime-type>", filename: "<filename>", path: "<upload-path>"}

Run the migration with mix ecto.migrate.

Next comes the model.

defmodule MySite.Image
    use MySite.Web, :model

    schema "images" do
        field :image, :any, virtual: true
        field :image_data, :binary
        field :image_name, :string
        field :image_type, :string
    end

    def changeset(struct, params \\ %{}) do
        struct
        |> cast(params, [:image]
    end
end

Here we just adding a simple schema and changeset declaration. If you notice, we’re just casting a virtual image field which will do to get us started.

We’ll come back to this file later.

 

Uploading the image file

Uploading the image file is really as simple as adding a file input control to your form template and passing the params to the relevant controller actions.

The form

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

    <div class="form-group">
        <%= label f, :image, class: "control-label" %>
        <%= file_input f, :image, class: "form-control" %>
        <%= error_tag f, :image %>
    </div>

    ...

<% end %>

Important points to note are that you need to pass [multipart: true] to the form otherwise the file_input control doesn’t work.

When data is submitted to the server, you can access the above given plug in the same way as all other submitted form data.

alias MySite.Image

def create(conn, %{"image" => image_params}) do
    changeset = Image.changeset(%Image{}, image_params)

    case Repo.insert(changeset) do
        {:ok, _entry} ->
            conn
            |> put_flash(:info, "Image saved successfully.")
            |> redirect(to: image_path(conn, :index))
        {:error, changeset} ->
            render(conn, "new.html", changeset: changeset)
    end
end

 

Hiving the image file away into the DB

Now that we can handle the image upload in the controller, we need to turn our attention to the model. If you remember, we left it casting a virtual image field.

Lets now change the changeset pipeline to handle the image data

def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:image])
    |> store_image
end

defp store_image(changeset) do
    case changeset do
        %Ecto.Changeset{valid?: true, changes: %{image: image}} ->
            changeset
            |> put_change(:image_type, image.content_type)
            |> put_change(:image_name, image.filename)
            |> put_change(:image_data, :base64.encode(File.read!(image.path)))
        _ -> changeset
    end
end

Ok, so here, we’re checking that firstly the changeset is valid at this point, and that there is an image in the changes field of the changeset. If this pattern match is successful, then we can work on the image data. If not, then we pass on the changeset like a good citizen.

As you can see, from the Plug we’re pulling out the content_type, filename and path fields from the submitted image plug.

For the actual image data itself, we need to open the file at the given temporary path and then base64 encode the contents so that it can be saved in the given DB field.

We then put this data back into the changeset with the put_change function.

You can then validate the added fields as required in the changeset pipeline as standard.

 

Getting the image back out again

So thats the image stored away as a binary in the DB. Great. Job done.

Well not quite. The image is stored, but you’ll want it back out again to show in all its glory to your users.

The target here is to be able to provide image tags with a useable, routeable URL, something like /image/<image-id>.

So lets add that to our router file

scope "/", MySite do
    ...
    get "/image/:id", ImageController, :index
    ...
end

Now that we referenced a controller, lets put one in.

defmodule MySite.ImageController do
    use MySite.Web, :controller

    alias MySite.Image

    def index(conn, %{"id" => id}) do
        image = Repo.get(Image, id)
        conn
        |> encode_image(image)
    end

    defp encode_image(conn, image) do
        conn
        |> put_layout(:none)
        |> put_resp_content_type(image.image_type)
        |> resp(200, :base64.decode(image.image_data))
        |> send_resp()
        |> halt()
    end
end

Here we’re telling Phoenix that

  • we don’t need a layout
  • what the content type is
  • the required http status code
  • the response body which is the base64 decoded file data from the DB.
  • to send the response

and then to just halt because the image is sent with the send_resp() function.

 

Displaying the image to the world

All that needs to happen now is that the image id needs to be obtained. How you do that is really entirely up to you.

As an controller (example for completeness):

defmodule MySite.PageController do
    use MySite.Web, :controller

    alias MySite.Image

    def index(conn, _params) do
        images = Repo.all(from(Image, select: [:id]))
        render conn, "index.html", images: images
    end
end

With view:

defmodule MySite.PageVeiw do
    use MySite.Web, :view
end

And template:

<%= for image <- @images do %>
    <img src="/image/<%=image.id%>" />
<% end %>

And thats it. Just run the server as normal and your images should pull from the DB and display where called.