Skip to content

Instantly share code, notes, and snippets.

@Wigny
Last active May 17, 2023 17:31
Show Gist options
  • Save Wigny/471acb1049b3d5da02fb2df87803d632 to your computer and use it in GitHub Desktop.
Save Wigny/471acb1049b3d5da02fb2df87803d632 to your computer and use it in GitHub Desktop.
Phoenix external upload to GCS
Application.put_env(:uploader, Uploader.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4001],
server: true,
render_errors: [
formats: [html: Uploader.ErrorHTML],
layout: false
],
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Mix.install([
{:plug_cowboy, "~> 2.6"},
{:jason, "~> 1.0"},
{:phoenix, "1.7.2"},
{:phoenix_live_view, "0.18.18"},
{:gcs_signed_url, "~> 0.4"}
])
defmodule Uploader.ErrorHTML do
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
defmodule Uploader.Layouts do
use Phoenix.Component
def render("root.html", assigns) do
~H"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={Phoenix.Controller.get_csrf_token()} />
<.live_title>Uploader</.live_title>
<script src="https://unpkg.com/phoenix@1.7.2/priv/static/phoenix.min.js"></script>
<script src="https://unpkg.com/phoenix_live_view@0.18.18/priv/static/phoenix_live_view.min.js"></script>
<script type="module">
import { createUpload } from "https://unpkg.com/@mux/upchunk@3.1.0/dist/upchunk.mjs";
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
const liveSocket = new LiveView.LiveSocket("/live", Phoenix.Socket, {
params: { _csrf_token: csrfToken },
uploaders: {
UpChunk: (entries, onViewError) => {
entries.forEach(entry => {
const { file, meta: { endpoint, headers } } = entry;
const upload = createUpload({ endpoint, headers, file });
onViewError(() => upload.pause());
upload.on("error", (e) => {
entry.error(e.detail.message);
});
upload.on("progress", (e) => {
if (e.detail < 100) { entry.progress(e.detail); }
});
upload.on("success", () => {
entry.progress(100);
});
});
}
}
});
liveSocket.connect();
</script>
</head>
<body>
<%= @inner_content %>
</body>
</html>
"""
end
end
defmodule Uploader.HomeLive do
use Phoenix.LiveView
@google_application_credentials System.fetch_env!("GOOGLE_APPLICATION_CREDENTIALS")
def mount(_params, _session, socket) do
{
:ok,
allow_upload(socket, :images,
accept: ~w(image/*),
max_entries: 1,
auto_upload: true,
external: &presign_upload/2,
progress: &handle_progress/3
)
}
end
def render(assigns) do
~H"""
<div id="info"><%= Phoenix.Flash.get(@flash, :info) %></div>
<form phx-submit="save" phx-change="validate">
<.live_file_input upload={@uploads.images} />
</form>
"""
end
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
def handle_event("save", _params, socket) do
{:noreply, socket}
end
defp presign_upload(entry, socket) do
client = GcsSignedUrl.Client.load_from_file(@google_application_credentials)
bucket = "bucket"
headers = ["Content-Type": entry.client_type]
sign_opts = [verb: "PUT", expires: 1800, headers: headers]
endpoint = GcsSignedUrl.generate_v4(client, bucket, "test/#{entry.client_name}", sign_opts)
{:ok, %{uploader: "UpChunk", endpoint: endpoint, headers: Map.new(headers)}, socket}
end
defp handle_progress(:images, %{done?: true} = entry, socket) do
{:noreply, put_flash(socket, :info, "File `#{entry.client_name}` uploaded!")}
end
defp handle_progress(:images, _entry, socket) do
{:noreply, socket}
end
end
defmodule Uploader.Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, {Uploader.Layouts, :root})
plug(:protect_from_forgery)
end
scope "/", Uploader do
pipe_through(:browser)
live("/", HomeLive, :index)
end
end
defmodule Uploader.Endpoint do
use Phoenix.Endpoint, otp_app: :uploader
@session_options [
store: :cookie,
key: "_uploader_key",
signing_salt: "aaaaaaaa",
same_site: "Lax"
]
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
plug(Plug.Session, @session_options)
plug(Uploader.Router)
end
{:ok, _} = Supervisor.start_link([Uploader.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment