Skip to content

Commit

Permalink
Implements basic Supabase Storage integration (#2)
Browse files Browse the repository at this point in the history
* feat: create a connection structure and remove unused functions

* feat: supabase storage basic integration

* fix: correctly upload and downloads files
  • Loading branch information
zoedsoupe authored Sep 16, 2023
1 parent 0d9d5b5 commit 0df49b4
Show file tree
Hide file tree
Showing 22 changed files with 1,457 additions and 58 deletions.
97 changes: 97 additions & 0 deletions apps/supabase_fetcher/lib/supabase/connection.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
defmodule Supabase.Connection do
@moduledoc """
Defines the connection to Supabase, it is an Agent that holds the connection
information and the current bucket.
To start the connection you need to call `Supabase.Connection.start_link/1`:
iex> Supabase.Connection.start_link(name: :my_conn, conn_info: %{base_url: "https://myapp.supabase.io", api_key: "my_api_key"})
{:ok, #PID<0.123.0>}
But usually you would add the connection to your supervision tree:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
conn_info = %{base_url: "https://myapp.supabase.io", api_key: "my_api_key"}
children = [
{Supabase.Connection, conn_info: conn_info, name: :my_conn}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Once the connection is started you can use it to perform operations on the
storage service, for example to list all the buckets:
iex> conn = Supabase.Connection.fetch_current_bucket!(:my_conn)
iex> Supabase.Storage.list_buckets(conn)
{:ok, [
%Supabase.Storage.Bucket{
allowed_mime_types: nil,
file_size_limit: nil,
id: "my-bucket-id",
name: "my-bucket",
public: true
}
]}
Notice that you can start multiple connections, each one with different
credentials, and you can use them to perform operations on different buckets!
"""

use Agent

@type base_url :: String.t()
@type api_key :: String.t()
@type access_token :: String.t()
@type bucket :: struct

@fields ~w(base_url api_key access_token bucket)a

def start_link(args) do
name = Keyword.fetch!(args, :name)
conn_info = Keyword.fetch!(args, :conn_info)

Agent.start_link(fn -> parse_init_args(conn_info) end, name: name)
end

defp parse_init_args(conn_info) do
conn_info
|> Map.take(@fields)
|> Map.put_new(:access_token, conn_info[:api_key])
end

def fetch_current_bucket!(conn) do
Agent.get(conn, &Map.get(&1, :bucket)) ||
raise "No current bucket configured on your connection"
end

def get_base_url(conn) do
Agent.get(conn, &Map.get(&1, :base_url))
end

def get_api_key(conn) do
Agent.get(conn, &Map.get(&1, :api_key))
end

def get_access_token(conn) do
Agent.get(conn, &Map.get(&1, :access_token))
end

def put_access_token(conn, token) do
Agent.update(conn, &Map.put(&1, :access_token, token))
end

def put_current_bucket(conn, bucket) do
Agent.update(conn, &Map.put(&1, :bucket, bucket))
end

def remove_current_bucket(conn) do
Agent.update(conn, &Map.delete(&1, :bucket))
end
end
68 changes: 15 additions & 53 deletions apps/supabase_fetcher/lib/supabase/fetcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ defmodule Supabase.Fetcher do
Task.shutdown(task)
end)

{status, stream}
case {status, stream} do
{200, stream} -> {:ok, stream}
{s, _} when s >= 400 -> {:error, :not_found}
{s, _} when s >= 500 -> {:error, :server_error}
end
end

defp spawn_stream_task(%Finch.Request{} = req, ref, opts) do
Expand All @@ -77,10 +81,10 @@ defmodule Supabase.Fetcher do
end)
end

defp receive_stream(ref) do
defp receive_stream({ref, _task} = payload) do
receive do
{:chunk, {:data, data}, ^ref} -> {[data], ref}
{:done, ^ref} -> {:halt, ref}
{:chunk, {:data, data}, ^ref} -> {[data], payload}
{:done, ^ref} -> {:halt, payload}
end
end

Expand Down Expand Up @@ -177,17 +181,9 @@ defmodule Supabase.Fetcher do
"""
@impl true
def upload(method, url, file, headers \\ []) do
alias Multipart.Part

multipart = Multipart.add_part(Multipart.new(), Part.file_field(file, true))
body_stream = Multipart.body_stream(multipart)
content_length = Multipart.content_length(multipart)

content_headers = [
{"content-type", "application/json"},
{"content-length", to_string(content_length)}
]

body_stream = File.stream!(file, [{:read_ahead, 4096}], 1024)
%File.Stat{size: content_length} = File.stat!(file)
content_headers = [{"content-length", to_string(content_length)}]
headers = merge_headers(headers, content_headers)
conn = new_connection(method, url, {:stream, body_stream}, headers)

Expand All @@ -196,56 +192,22 @@ defmodule Supabase.Fetcher do
|> format_response()
end

@doc """
Simple convenience taht given a `Supabase.Connection`, it will return the full URL
to your Supabase API. For more information, check the
[Supabase.Connection](https://hexdocs.pm/supabase_potion/Supabase.Connection.html)
documentation.
You can also pass the base URL of your Supabase API and the URL you want to request
directly.
## Examples
iex> Supabase.Fetcher.get_full_url(conn, "/rest/v1/tables")
"https://<your-project>.supabase.co/rest/v1/tables"
"""
def get_full_url(conn, url) when is_atom(conn) do
base_url = conn.get_base_url()
URI.merge(base_url, url)
end

def get_full_url(base_url, url) when is_binary(base_url) do
URI.merge(base_url, url)
def get_full_url(base_url, path) do
URI.merge(base_url, path)
end

@doc """
Convenience function that given a `Supabase.Connection`, it will return the headers
to be used in a request to your Supabase API. For more information, check the
[Supabase.Connection](https://hexdocs.pm/supabase_potion/Supabase.Connection.html)
documentation.
Also you can pass the API key and the access token directly.
Convenience function that given a `apikey` and a optional ` token`, it will return the headers
to be used in a request to your Supabase API.
## Examples
iex> Supabase.Fetcher.apply_conn_headers(conn)
[{"apikey", "apikey-value"}, {"authorization", "Bearer token-value"}]
iex> Supabase.Fetcher.apply_conn_headers("apikey-value")
[{"apikey", "apikey-value"}, {"authorization", "Bearer apikey-value"}]
iex> Supabase.Fetcher.apply_conn_headers("apikey-value", "token-value")
[{"apikey", "apikey-value"}, {"authorization", "Bearer token-value"}]
"""
def apply_conn_headers(conn, additional_headers \\ []) when is_atom(conn) do
conn_headers = [
{"apikey", conn.get_api_key()},
{"authorization", conn.get_access_token()}
]

merge_headers(conn_headers, additional_headers)
end

def apply_headers(api_key, token \\ nil, headers \\ []) do
conn_headers = [
Expand Down
5 changes: 2 additions & 3 deletions apps/supabase_fetcher/lib/supabase/fetcher_behaviour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ defmodule Supabase.FetcherBehaviour do
@callback put(url, body, headers) :: result
@callback delete(url, body, headers) :: result
@callback upload(method, url, Path.t(), headers) :: result
@callback stream(url, headers, keyword) :: {status, stream}
when status: integer,
stream: Stream.t()
@callback stream(url, headers, keyword) :: {:ok, stream} | {:error, reason}
when stream: Stream.t()
end
3 changes: 1 addition & 2 deletions apps/supabase_fetcher/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ defmodule Supabase.Fetcher.MixProject do
defp deps do
[
{:finch, "~> 0.16"},
{:jason, "~> 1.4"},
{:multipart, "~> 0.1.0"}
{:jason, "~> 1.4"}
]
end
end
4 changes: 4 additions & 0 deletions apps/supabase_storage/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions apps/supabase_storage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
supabase_storage-*.tar

# Temporary files, for example, from tests.
/tmp/
1 change: 1 addition & 0 deletions apps/supabase_storage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Supabase Storage
Loading

0 comments on commit 0df49b4

Please sign in to comment.