# Direct File uploads with Phoenix Liveview and Cloudflare R2.

It's been really hard to figure this out and I had to cobble together many sources to get it working. Enjoy!

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1704986791701/eed952d1-1b8e-414b-b277-febe1e1b9cf0.png align="center")

We're going to build a direct image upload to Cloudflare B2 from the browser. That way the file doesn't travel to your servers and slow them down.

## Create your bucket and set CORS

You'll need to set your bucket to allow public access, and set some CORS rules to allow remote PUT requests.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1704986934884/d13b2e5c-4bc0-4023-98a1-5fe2b94885d9.png align="center")

And edit your CORS policy for that bucket:

```json
[
  {
    "AllowedOrigins": [
      "http://localhost:4000",
      "https://app.onrender.com"
    ],
    "AllowedMethods": [
      "GET",
      "PUT",
      "POST"
    ],
    "AllowedHeaders": [
      "*"
    ],
    "ExposeHeaders": []
  }
]
```

Finally, you'll need these environment variables so set them for your project locally.

```bash
export CLOUDFRONT_ACCOUNT_ID="xxx"
export CLOUDFRONT_BUCKET_NAME="xxx"
export CLOUDFRONT_R2_ACCESS_KEY_ID="xxx"
export CLOUDFRONT_R2_SECRET_ACCESS_KEY="xxx"
```

## Install mix deps

```elixir
{:ex_aws, "~> 2.5"},
{:ex_aws_s3, "~> 2.5"},
{:aws_signature, "~> 0.3.1"}
```

## Create simple\_s3\_upload.ex helper module

This file was modified by someone else, many thanks to him!

```elixir
defmodule MyApp.SimpleS3Upload do
  @moduledoc """
  Below is code from Chris McCord, modified for Cloudflare R2

  https://gist.github.com/chrismccord/37862f1f8b1f5148644b75d20d1cb073

  """
  @one_hour_seconds 3600

  @doc """
    Returns `{:ok, presigned_url}` where `presigned_url` is a url string

  """
  def presigned_put(config, opts) do
    key = Keyword.fetch!(opts, :key)
    expires_in = Keyword.get(opts, :expires_in, @one_hour_seconds)
    uri = "#{config.url}/#{URI.encode(key)}"

    url =
      :aws_signature.sign_v4_query_params(
        config.access_key_id,
        config.secret_access_key,
        config.region,
        "s3",
        :calendar.universal_time(),
        "PUT",
        uri,
        ttl: expires_in,
        uri_encode_path: false,
        body_digest: "UNSIGNED-PAYLOAD"
      )

    {:ok, url}
  end
end
```

---

Chin up boys, that was the hard part. Let's push some files!

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1704987241885/f6edc1bb-e1dd-4512-bab2-223252ffaa2d.gif align="center")

---

## Allow\_upload for your socket

We need to let liveview know we want some files.

```elixir
def update(assigns, socket) do
  {:ok,
   socket
   |> assign(:uploaded_files, [])
   |> allow_upload(:profile_picture,
     accept: ~w(.jpg .jpeg .png),
     max_entries: 2,
     external: &presign_upload/2
   )
   |> assign_form(changeset)}
end

defp presign_upload(entry, socket) do
  filename = "#{entry.client_name}"
  key = "public/#{Nanoid.generate()}-#{filename}"

  config = %{
    region: "auto",
    access_key_id: System.get_env("CLOUDFRONT_R2_ACCESS_KEY_ID"),
    secret_access_key: System.get_env("CLOUDFRONT_R2_SECRET_ACCESS_KEY"),
    url:
      "https://#{System.get_env("CLOUDFRONT_BUCKET_NAME")}.#{System.get_env("CLOUDFRONT_ACCOUNT_ID")}.r2.cloudflarestorage.com"
  }

  {:ok, presigned_url} =
    MyApp.SimpleS3Upload.presigned_put(config,
      key: key,
      content_type: entry.client_type,
      max_file_size: socket.assigns.uploads[entry.upload_config].max_file_size
    )

  meta = %{
    uploader: "S3",
    key: key,
    url: presigned_url
  }

  {:ok, meta, socket}
end
```

## The HTML for your form.

The dudes at Phoenix Framework core team built some awesome helpers for us. Don't reinvent the wheel!

```elixir
# Inside your <.simple_form...
<.live_file_input upload={@uploads.profile_picture} />
<%= for entry <- @uploads.profile_picture.entries do %>
  <article class="upload-entry">
    <figure>
      <.live_img_preview entry={entry} />
      <figcaption><%= entry.client_name %></figcaption>
    </figure>

    <%!-- entry.progress will update automatically for in-flight entries --%>
    <progress value={entry.progress} max="100"><%= entry.progress %>%</progress>

    <%!-- a regular click event whose handler will invoke Phoenix.LiveView.cancel_upload/3 --%>
    <button
      type="button"
      phx-click="cancel-upload"
      phx-value-ref={entry.ref}
      phx-target={@myself}
      aria-label="cancel"
    >
      &times;
    </button>

    <%!-- Phoenix.Component.upload_errors/2 returns a list of error atoms --%>
    <%= for err <- upload_errors(@uploads.profile_picture, entry) do %>
      <p class="alert alert-danger"><%= inspect(err) %></p>
    <% end %>
  </article>
<% end %>
```

## Finally saving the file's URL in your database schema.

Just `consume_uploaded_entries` and you will have access to the final URL.

```elixir
defp save_company(socket, :new, company_params) do
  uploaded_files =
    consume_uploaded_entries(socket, :profile_picture, fn %{key: key}, _entry ->
      "https://pub-757575757575757575.r2.dev/#{key}"
    end)

  company_params = Map.put(company_params, "profile_picture_url", List.first(uploaded_files))
  Companies.create_company(company_params) 
end
```

And that it's. Enjoy significantly cheaper storage and no egress fees with Cloudflare R2 storage.

---

# If this helped you, follow me on Twitter/X!

[https://twitter.com/yeyoparadox](https://twitter.com/yeyoparadox)
