Uploading images to Heroku using Phoenix
Posted on June 23, 2017 by Clive in Elixir, Heroku, 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.
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. 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). 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. 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. This will generate a file in the standard location for 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: 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… Run the migration with Next comes the model. 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 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 Important points to note are that you need to pass When data is submitted to the server, you can access the above given plug in the same way as all other submitted form data. 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 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 You can then validate the added fields as required in the changeset pipeline as standard. 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 So lets add that to our router file Now that we referenced a controller, lets put one in. Here we’re telling Phoenix that and then to just halt because the image is sent with the send_resp() function. 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): With view: And template: And thats it. Just run the server as normal and your images should pull from the DB and display where called.Heroku
Setting the scene.
Flashback to “the good ol’ days”™
Preparing the DB and Phoenix
mix ecto.gen.migration add_image_uploads
priv/repo/migrations
def change do
create table(:images) do
add :image_data, :binary
add :image_name, :string
add :image_type, :string, size: 20
end
end
%Plug.Upload{content_type: "<mime-type>", filename: "<filename>", path: "<upload-path>"}
mix ecto.migrate
.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
Uploading the image file
<%= 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 %>
[multipart: true]
to the form otherwise the file_input control doesn’t work.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
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
put_change
function.Getting the image back out again
/image/<image-id>
.scope "/", MySite do
...
get "/image/:id", ImageController, :index
...
end
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
Displaying the image to the world
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
defmodule MySite.PageVeiw do
use MySite.Web, :view
end
<%= for image <- @images do %>
<img src="/image/<%=image.id%>" />
<% end %>