Skip to content

Commit

Permalink
move the high level explanation to fragments instead
Browse files Browse the repository at this point in the history
  • Loading branch information
kw7oe committed Jul 29, 2023
1 parent 11ca4f2 commit 407e97d
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 104 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: "CORS issue using Phoenix LiveView to direct upload to Backblaze or Cloudflare R2"
date: 2023-07-29T23:41:20+08:00
tags: ["phoenix", "liveview"]
---

Phoenix LiveView has an amazing documentation on implementing direct upload to S3 in
the [official documentation][0]. It works well if you are using AWS S3. However, you might
want to use it with AWS S3 alternatives like Backblaze and Cloudflare R2 that is S3
API compatible.

When attempting to follow the guide above in the Phoenix LiveView documentation, you'll
quickly realize following the steps doesn't end up with a working solution. You'll face
some CORS issue.

Then, you update the CORS configuration in the cloud storage settings and expecting it work.

But it doesn't either.

### Problem

So, what went wrong?

It turns out that, most of the AWS S3 alternatives aren't completely S3 API compatible. While
they support most of the S3 API, there are always some caveats.

In our case, it's because the documentation implement direct upload to S3 through
the `POST` method with native HTML form to a presigned URL, which is currently not
supported in some of the AWS S3 alternatives like Cloudflare R2 and Backblaze.

For Cloudflare R2, the reasoning can be found under the ["Supported HTTP methods" section
of the Cloudflare R2 Presigned URLs][1] documentation:


> ...`POST`, which performs uploads via native HTML forms, is not currently supported.
For Backblaze, while the documentation doesn't point it out explicitly about this, it's actually mentioned
in one of the [ElixirForum dicussion about Backblaze and Phoenix LiveView Uploads][2]:

> ... B2 does not support POST for the S3 PutObject operation.
If we take a look at the S3 Put Object documentation for Backblaze [here][3], it doesn mentioned the
HTTP method supported is `PUT`:

> ...`PUT` https://s3.<your-region>.backblazeb2.com/:bucket/:key
To conclude, it doesn't work because, in the Phoenix LiveView official documentation,
it is using the `POST` method to upload the file directly to AWS S3, which isn't supported.

### Solution

To resolve the above issue, we have to:

- Generate the presigned URL with `PUT` method. In the documentation, it's generating a `POST` presigned URL underneath.
- Update the `Uploader` JavaScript implementation to make a `PUT` HTTP requests instead of `POST` with form.

Now, these information should be sufficient for you to resolve your bug. If not, you can refer the following
for code exmaples from ElixirForum:

- [Using file upload in Phoenix Liveview with Cloudflare R2](https://elixirforum.com/t/using-file-upload-in-phoenix-liveview-with-cloudflare-r2/56182/2)
- [Backblaze and Phoenix LiveView uploads](https://elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/20)

[0]: https://hexdocs.pm/phoenix_live_view/uploads-external.html
[1]: https://developers.cloudflare.com/r2/api/s3/presigned-urls/#supported-http-methods
[2]: https://elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/8
[3]: https://www.backblaze.com/apidocs/s3-put-object
199 changes: 95 additions & 104 deletions content/posts/direct-file-upload-with-phoenix-live-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,6 @@ draft: true
tag: ["phoenix", "liveview"]
---

Phoenix LiveView has an amazing documentation on implementing direct upload to S3 in
the [official documentation][0]. It works well if you are using AWS S3. However, you might
want to use it with AWS S3 alternatives like Backblaze and Cloudflare R2 that is S3
API compatible.

When attempting to follow the guide above in the Phoenix LiveView documentation, you'll
quickly realize following the steps doesn't end up with a working solution. You'll face
some CORS issue.

Then, you update the CORS configuration in the cloud storage settings and expecting it work.

But it doesn't either.

In this post, I'm going to walk you through the following:

- [Summary](#summary)
- [Problem](#problem)
- [Solution](#solution)
- [Step by Step Guide](#step-by-step-guide)

## Summary

The TLDR; version of this is, `POST` with form aren't supported for presigned URL in
some of these AWS S3 alternatives. To resolve it, use a `PUT` instead.

### Problem

Most of the AWS S3 alternatives aren't completely S3 API compatible. While they support
most of the S3 API, there are always some caveats.

In our case, it's because the documentation implement direct upload to S3 through
the `POST` method with native HTML form to a presigned URL, which is currently not
supported in some of the AWS S3 alternatives like Cloudflare R2 and Backblaze.

For Cloudflare R2, the reasoning can be found under the ["Supported HTTP methods" section
of the Cloudflare R2 Presigned URLs][1] documentation:


> ...`POST`, which performs uploads via native HTML forms, is not currently supported.
For Backblaze, while the documentation doesn't point it out explicitly about this, it's actually mentioned
in one of the [ElixirForum dicussion about Backblaze and Phoenix LiveView Uploads][2]:

> ... B2 does not support POST for the S3 PutObject operation.
If we take a look at the S3 Put Object documentation for Backblaze [here][3], it doesn mentioned the
HTTP method supported is `PUT`:

> ...`PUT` https://s3.<your-region>.backblazeb2.com/:bucket/:key
To conclude, it doesn't work because, in the Phoenix LiveView official documentation,
it is using the `POST` method to upload the file directly to AWS S3, which isn't supported.

### Solution

To resolve the above issue, we have to:

- Generate the presigned URL with `PUT` method. In the documentation, it's generating a `POST` presigned URL underneath.
- Update the `Uploader` JavaScript implementation to make a `PUT` HTTP requests instead of `POST` with form.

Now, these information should be sufficient for you to resolve your bug. If not, you can continue follow
through the below step by step guide.

## Step by Step Guide

The title would be clickbaity without a full tutorial on implementing direct file upload to
Backblaze and Cloudflare R2 with Phoenix LiveView. So here we go.

Expand Down Expand Up @@ -128,38 +63,81 @@ basic CRUD generated by Phoenix for our `images` table.
### Direct upload

Now let the real work begin. Let's start with copying the `UploadLive` from
the [Phoenix LiveView Uploads documentation][4]:
the [Phoenix LiveView Uploads documentation][0]:

```heex
<%!-- lib/gallery_web/live/upload_live.html.heex --%>
```html
<h1 class="text-2xl font-bold mb-4">Upload Images</h1>

<form id="upload-form" phx-submit="save" phx-change="validate">
<.live_file_input upload={@uploads.avatar} />
<button type="submit">Upload</button>
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div class="sm:grid sm:border-t sm:border-gray-200 sm:pt-5 mx-auto">
<section class="mt-1 sm:mt-0" phx-drop-target={@uploads.avatar.ref}>
<div class="grid grid-cols-4 gap-4 mb-4">
<%= for entry <- @uploads.avatar.entries do %>
<article class="upload-entry">
<figure>
<.live_img_preview entry={entry} />
<figcaption><%= entry.client_name %></figcaption>
</figure>

<progress value={entry.progress} max="100"><%= entry.progress %>%</progress>

<button
type="button"
phx-click="cancel-upload"
phx-value-ref={entry.ref}
aria-label="cancel"
>
&times;
</button>

<%= for err <- upload_errors(@uploads.avatar, entry) do %>
<p class="alert alert-danger"><%= error_to_string(err) %></p>
<% end %>
</article>
<% end %>
</div>

<%= for err <- upload_errors(@uploads.avatar) do %>
<p class="alert alert-danger"><%= error_to_string(err) %></p>
<% end %>

<div class="flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg
class="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
</path>
</svg>

<div class="flex text-sm text-gray-600">
<p class="pl-1">Drag and drop your images here</p>
</div>
<p class="text-xs text-gray-500">
Photos up to 20MB
</p>
</div>
</div>
</section>
</div>
</div>

<div class="mt-4">
<p class="mb-4">or</p>
<.live_file_input upload={@uploads.avatar} />
<.button class="mt-4 float-right">Upload</.button>
</div>
</form>
<section phx-drop-target={@uploads.avatar.ref}>
<%= for entry <- @uploads.avatar.entries do %>
<article class="upload-entry">
<figure>
<.live_img_preview entry={entry} />
<figcaption><%= entry.client_name %></figcaption>
</figure>
<progress value={entry.progress} max="100"> <%= entry.progress %>% </progress>
<button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref} aria-label="cancel">&times;</button>
<%= for err <- upload_errors(@uploads.avatar, entry) do %>
<p class="alert alert-danger"><%= error_to_string(err) %></p>
<% end %>
</article>
<% end %>
<%= for err <- upload_errors(@uploads.avatar) do %>
<p class="alert alert-danger"><%= error_to_string(err) %></p>
<% end %>
</section>
```

and
Expand All @@ -172,9 +150,9 @@ defmodule GalleryWeb.UploadLive do
@impl Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}
end

@impl Phoenix.LiveView
Expand All @@ -190,8 +168,15 @@ defmodule GalleryWeb.UploadLive do
@impl Phoenix.LiveView
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join([:code.priv_dir(:my_app), "static", "uploads", Path.basename(path)])
consume_uploaded_entries(socket, :avatar, fn %{path: path}, entry ->
dest =
Path.join([
:code.priv_dir(:gallery),
"static",
"uploads",
Path.basename(entry.client_name)
])

File.cp!(path, dest)
{:ok, ~p"/uploads/#{Path.basename(dest)}"}
end)
Expand All @@ -205,15 +190,21 @@ defmodule GalleryWeb.UploadLive do
end
```

After copy and pasting these into files, we'll also have to do the following:

1. Create a `priv/static/uploads` directory as our code will be copying the uploaded files
to that directory.
```bash
mkdir priv/static/uploads
```

2. Update the `GalleryWeb.static_path()` function to include `uplaods`:

```elixir
- def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
+ def static_paths, do: ~w(assets fonts images uploads favicon.ico robots.txt)
```




[0]: https://hexdocs.pm/phoenix_live_view/uploads-external.html
[1]: https://developers.cloudflare.com/r2/api/s3/presigned-urls/#supported-http-methods
[2]: https://elixirforum.com/t/backblaze-and-phoenix-liveview-uploads/57153/8
[3]: https://www.backblaze.com/apidocs/s3-put-object
[4]: https://hexdocs.pm/phoenix_live_view/uploads.html
[0]: https://hexdocs.pm/phoenix_live_view/uploads.html

0 comments on commit 407e97d

Please sign in to comment.