Sheharyar Naseer

File Uploads in Elixir with Arc and Google Cloud Storage


Arc is one of the go-to libraries for file uploads and attachments in Elixir, most commonly used with Ecto. While there are plenty of documentation and guides on using it with local storage or AWS, there’s not enough detailed information available when you want to use Google Cloud Storage. This post is meant as a quick reference guide for using GCS with Arc.

Set up Arc for Local Storage

If you’ve already set up Arc (possibly using it with Local Storage), you can skip this section. If not, follow along to get started. First add the :arc_ecto dependency (if you don’t want Ecto support, just use :arc instead):

defp deps do
  [{:arc_ecto, "0.11.2"}]
end

Create a new Arc Definition file:

# lib/my_app/uploads/image_attachment.ex

defmodule MyApp.Uploads.ImageAttachment do
  use Arc.Definition
  use Arc.Ecto.Definition

  @versions [:original, :thumb]
  @extension_whitelist ~w[.jpg .jpeg .gif .png]

  def transform(:thumb, _scope) do
    {:convert, "-strip -thumbnail 100x100^ -gravity center -extent 100x100 -format png", :png}
  end

  def validate({file, _}) do
    file_extension = file.file_name |> Path.extname() |> String.downcase()
    Enum.member?(@extension_whitelist, file_extension)
  end

  def storage_dir(_version, {_file, scope}) do
    "uploads/images/#{scope.id}"
  end

  def filename(version, {_file, _scope}) do
    to_string(version)
  end
end

The ImageAttachment definition above is for image uploads only. For other file types, you can modify it according to your requirements. To use it with Ecto schemas, add the field (and the appropriate migration) referencing the type:

defmodule MyApp.User do
  use Ecto.Schema
  alias MyApp.Uploads.ImageAttachment

  schema "users" do
    field :name, :string
    field :avatar, ImageAttachment.Type

    # ...
  end

  def changeset(struct, attrs) do
    struct
    |> cast(attrs, [:name, ...])
    |> cast_attachments(attrs, [:avatar])
  end
end

A quick way to test uploads is to run this in IEx:

# Define a module to mock file uploads from local files
defmodule MyApp.Mock do
  def file_upload(path) do
    %Plug.Upload{
      path: Path.expand(path),
      filename: Path.basename(path),
      content_type: MIME.from_path(path),
    }
  end
end

# Try "uploading" a local file as a user avatar
avatar = Mock.file_upload("~/Pictures/some-image.png")

%User{}
|> User.changeset(%{name: "Shey", avatar: avatar})
|> Repo.insert()

See Arc.Ecto for more details. Once you have uploads correctly working for local storage, we can then move on to GCS support.

Configure Google Cloud Storage

We need to set up Google Cloud Storage before we can start uploading files to it:

  1. Create a Project in Google Cloud Console (if you haven’t already)

  2. Create a new Bucket in Cloud Storage.

    • Using something like {myapp}-{env}-uploads for the bucket id is a good idea.

    • Choose your bucket’s region and storage class. Most common and recommended settings are “Multi-region” bucket with “Standard” storage class, but this might be different depending on your use-cases (make sure you understand pricing too).

    • For access control, “Fine-grained” is recommended which gives you best of both worlds, but again depends on your requirements. (You can skip advanced settings).

  3. Chances are that you want your uploaded files to be publicly accessible, so you should update the bucket permissions to reflect that:

    • After your bucket is created, click on “Permissions” tab
    • Click on the “Add members” button to show the right sidebar
    • In “New members”, add allUsers and under roles, select “Storage Object Viewer”, and save
  4. We also need to give your app a way to upload files to GCS. For that we need to create a Service Account with the correct permissions:

    • Go to IAM > Service Accounts in the console
    • Create a new Service account and give it an easily identifiable name and id
    • Under roles, add “Storage Object Creator”
    • You can skip “User Access”, but before finishing select “Create Key” and download it in JSON format (you can also always download it later)

I know, that was a lot of work to just upload some files to Google Cloud, but the good part is it’s over and we can get back to our Elixir app.

Enable GCS Support in Arc

First, we need to add the dependency for GCS storage provider in mix.exs:

defp deps do
  [
    {:arc_ecto, "0.11.2"},
    {:arc_gcs, "0.1.2"},
    # ...
  ]
end

Add the previously downloaded Service Account Key to your app, preferably at config/secrets/{env}/gcp.json. Important Note: You should absolutely not commit this file to Git (or any other version control). Make sure you add it to your .gitignore.

Now, update the Arc config to use GCS for file uploads with the service account key:

config :arc,
  storage: Arc.Storage.GCS,
  bucket: "myapp-dev-uploads"

config :goth,
  json: File.read!("config/secrets/dev/gcp.json")

Finally, update your ImageAttachment definition to send MIME (and any other custom) headers to GCP when uploading to identify the file:

defmodule MyApp.Uploads.ImageAttachment do
  use Arc.Definition
  use Arc.Ecto.Definition

  @versions [:original, :thumb]
  @extension_whitelist ~w[.jpg .jpeg .gif .png]

  def transform(:thumb, _scope) do
    {:convert, "-strip -thumbnail 100x100^ -gravity center -extent 100x100 -format png", :png}
  end

  def validate({file, _scope}) do
    file_extension = file.file_name |> Path.extname() |> String.downcase()
    Enum.member?(@extension_whitelist, file_extension)
  end

  def storage_dir(_version, {_file, scope}) do
    "uploads/images/#{scope.id}"
  end

  def filename(version, {_file, _scope}) do
    to_string(version)
  end

  # Add this:

  def gcs_object_headers(_version, {file, _scope}) do
    [content_type: MIME.from_path(file.file_name)]
  end
end

Try uploading a file again:

avatar = Mock.file_upload("~/Pictures/some-image.png")

user =
  %User{}
  |> User.changeset(%{name: "Shey", avatar: avatar})
  |> Repo.insert()

ImageAttachment.url({user.avatar, user})
# => "https://storage.googleapis.com/myapp-dev-uploads/....."

The above will return a public link to your uploaded image. You can also see all uploaded files in GCP Storage Browser.

This guide doesn’t cover the frontend aspect because assets can be uploaded via a variety of methods, which have already been covered extensively in the Arc docs and other places on the internet. Also, while the ImageAttachment definition above does cover the majority of cases, for other use-cases or for fine-grained configuration, you should again refer to the official Arc Github repo.