From 688cdf3753742618c73ae4f24c12b2628752b7db Mon Sep 17 00:00:00 2001 From: Zoey de Souza Pessanha Date: Tue, 10 Oct 2023 17:20:59 -0300 Subject: [PATCH] Split Supabase Storage (#13) * Refactor/project restructure (#11) * feat: remove umbrella project * refactor: use client to storage * fix: credo * refactor: documentation * feat: add bang option to init_client/1 * fix: improve ci * feat: supabase basic tests * feat: split supabase storage --- Earthfile | 1 - config/config.exs | 1 - config/runtime.exs | 5 - guides/storage.md | 5 + lib/supabase/application.ex | 18 +- lib/supabase/client.ex | 19 + lib/supabase/client/db.ex | 1 + lib/supabase/client/global.ex | 1 + lib/supabase/storage.ex | 636 ------------------ lib/supabase/storage/action_error.ex | 5 - lib/supabase/storage/bucket.ex | 127 ---- lib/supabase/storage/cache.ex | 97 --- lib/supabase/storage/cache_reloader.ex | 51 -- lib/supabase/storage/endpoints.ex | 51 -- .../storage/handlers/bucket_handler.ex | 133 ---- .../storage/handlers/object_handler.ex | 226 ------- lib/supabase/storage/object.ex | 75 --- lib/supabase/storage/object_options.ex | 57 -- lib/supabase/storage/search_options.ex | 60 -- lib/supabase/storage_behaviour.ex | 43 -- mix.exs | 5 +- mix.lock | 3 + 22 files changed, 32 insertions(+), 1588 deletions(-) delete mode 100644 config/config.exs delete mode 100644 config/runtime.exs delete mode 100644 lib/supabase/storage.ex delete mode 100644 lib/supabase/storage/action_error.ex delete mode 100644 lib/supabase/storage/bucket.ex delete mode 100644 lib/supabase/storage/cache.ex delete mode 100644 lib/supabase/storage/cache_reloader.ex delete mode 100644 lib/supabase/storage/endpoints.ex delete mode 100644 lib/supabase/storage/handlers/bucket_handler.ex delete mode 100644 lib/supabase/storage/handlers/object_handler.ex delete mode 100644 lib/supabase/storage/object.ex delete mode 100644 lib/supabase/storage/object_options.ex delete mode 100644 lib/supabase/storage/search_options.ex delete mode 100644 lib/supabase/storage_behaviour.ex diff --git a/Earthfile b/Earthfile index e3d8ede..93eed0d 100644 --- a/Earthfile +++ b/Earthfile @@ -25,7 +25,6 @@ unit-test: FROM +deps RUN MIX_ENV=test mix deps.compile COPY mix.exs mix.lock ./ - COPY --dir config ./ COPY --dir lib ./ COPY --dir test ./ RUN mix test diff --git a/config/config.exs b/config/config.exs deleted file mode 100644 index becde76..0000000 --- a/config/config.exs +++ /dev/null @@ -1 +0,0 @@ -import Config diff --git a/config/runtime.exs b/config/runtime.exs deleted file mode 100644 index f47a4e9..0000000 --- a/config/runtime.exs +++ /dev/null @@ -1,5 +0,0 @@ -import Config - -config :supabase, - cache_buckets?: false, - reload_interval: 60_000 diff --git a/guides/storage.md b/guides/storage.md index 18b99cf..1cce368 100644 --- a/guides/storage.md +++ b/guides/storage.md @@ -2,6 +2,11 @@ This module provides a set of Elixir functions that integrate seamlessly with Supabase's Storage API, allowing developers to perform various operations on buckets and objects. +For more detailed information and specialized documentation, refers to: + +- [supabase_storage](https://github.com/zoedsoupe/supabase_storage) +- [supabase storage docs](https://hexdocs.pm/supabase_storage) + ### Features 1. **Bucket Operations**: Easily create, list, empty, or remove buckets. diff --git a/lib/supabase/application.ex b/lib/supabase/application.ex index 08ad874..240aad2 100644 --- a/lib/supabase/application.ex +++ b/lib/supabase/application.ex @@ -3,8 +3,6 @@ defmodule Supabase.Application do use Application - alias Supabase.Storage - @finch_opts [name: Supabase.Finch, pools: %{:default => [size: 10]}] @impl true @@ -12,9 +10,7 @@ defmodule Supabase.Application do children = [ {Finch, @finch_opts}, if(manage_clients?(), do: Supabase.ClientSupervisor), - if(manage_clients?(), do: Supabase.ClientRegistry), - if(start_cache?(), do: {Storage.Cache, cache_max_size: cache_max_size()}), - if(start_cache?(), do: {Storage.CacheSupervisor, reload_interval: reload_interval()}) + if(manage_clients?(), do: Supabase.ClientRegistry) ] opts = [strategy: :one_for_one, name: Supabase.Supervisor] @@ -27,16 +23,4 @@ defmodule Supabase.Application do defp manage_clients? do Application.get_env(:supabase, :manage_clients, true) end - - defp cache_max_size do - Application.get_env(:supabase, :cache_max_size, 100) - end - - defp start_cache? do - Application.get_env(:supabase, :cache_buckets?) - end - - defp reload_interval do - Application.get_env(:supabase, :reload_interval, 60_000) - end end diff --git a/lib/supabase/client.ex b/lib/supabase/client.ex index f0cc115..f700cd5 100644 --- a/lib/supabase/client.ex +++ b/lib/supabase/client.ex @@ -116,6 +116,7 @@ defmodule Supabase.Client do |> cast_embed(:global, required: false) |> cast_embed(:auth, required: false) |> validate_required([:name]) + |> maybe_put_assocs() |> apply_action(:parse) end @@ -130,6 +131,24 @@ defmodule Supabase.Client do end end + defp maybe_put_assocs(%{valid?: false} = changeset), do: changeset + + defp maybe_put_assocs(changeset) do + auth = get_change(changeset, :auth) + db = get_change(changeset, :db) + global = get_change(changeset, :global) + + changeset + |> maybe_put_assoc(:auth, auth, %Auth{}) + |> maybe_put_assoc(:db, db, %Db{}) + |> maybe_put_assoc(:global, global, %Global{}) + end + + defp maybe_put_assoc(changeset, key, nil, default), + do: put_change(changeset, key, default) + + defp maybe_put_assoc(changeset, _key, _assoc, _default), do: changeset + def start_link(config) do name = Keyword.get(config, :name) client_info = Keyword.get(config, :client_info) diff --git a/lib/supabase/client/db.ex b/lib/supabase/client/db.ex index 0330e1d..7e9f172 100644 --- a/lib/supabase/client/db.ex +++ b/lib/supabase/client/db.ex @@ -18,6 +18,7 @@ defmodule Supabase.Client.Db do @type t :: %__MODULE__{schema: String.t()} @type params :: %{schema: String.t()} + @primary_key false embedded_schema do field(:schema, :string, default: "public") end diff --git a/lib/supabase/client/global.ex b/lib/supabase/client/global.ex index b2b6b6e..907500b 100644 --- a/lib/supabase/client/global.ex +++ b/lib/supabase/client/global.ex @@ -14,6 +14,7 @@ defmodule Supabase.Client.Global do @type t :: %__MODULE__{headers: Map.t()} @type params :: %{headers: Map.t()} + @primary_key false embedded_schema do field(:headers, {:map, :string}, default: %{}) end diff --git a/lib/supabase/storage.ex b/lib/supabase/storage.ex deleted file mode 100644 index 4f53ebc..0000000 --- a/lib/supabase/storage.ex +++ /dev/null @@ -1,636 +0,0 @@ -defmodule Supabase.Storage do - @moduledoc """ - Supabase.Storage Elixir Package - - This module provides integration with the Supabase Storage API, enabling developers - to perform a multitude of operations related to buckets and objects with ease. - - ## Features - - 1. **Bucket Operations**: Methods that allow the creation, listing, and removal of buckets. - 2. **Object Operations**: Functions designed to upload, download, retrieve object information, - and perform move, copy, and remove actions on objects. - - ## Usage - - You can start by creating or managing buckets: - - Supabase.Storage.create_bucket(client, "my_new_bucket") - - Once a bucket is set up, objects within the bucket can be managed: - - Supabase.Storage.upload_object(client, "my_bucket", "path/on/server.png", "path/on/local.png") - - ## Examples - - Here are some basic examples: - - # Removing an object - Supabase.Storage.remove_object(client, "my_bucket", "path/on/server.png") - - # Moving an object - Supabase.Storage.move_object(client, "my_bucket", "path/on/server1.png", "path/on/server2.png") - - Ensure to refer to method-specific documentation for detailed examples and explanations. - - ## Permissions - - Do remember to check and set the appropriate permissions in Supabase to make sure that the - operations can be performed without any hitches. - """ - - import Supabase.Client, only: [is_client: 1] - - alias Supabase.Client - alias Supabase.Client.Conn - alias Supabase.Storage.Bucket - alias Supabase.Storage.BucketHandler - alias Supabase.Storage.Object - alias Supabase.Storage.ObjectHandler - alias Supabase.Storage.ObjectOptions - alias Supabase.Storage.SearchOptions - - @behaviour Supabase.StorageBehaviour - - @doc """ - Retrieves information about all buckets in the current project. - - ## Notes - - * Policy permissions required - * `buckets` permissions: `select` - * `objects` permissions: none - - ## Examples - - iex> Supabase.Storage.list_buckets(client) - {:ok, [%Supabase.Storage.Bucket{...}, ...]} - - iex> Supabase.Storage.list_buckets(invalid_conn) - {:error, reason} - - """ - @impl true - def list_buckets(client) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - {:ok, BucketHandler.list(base_url, api_key, token)} - end - end - - @doc """ - Retrieves information about a bucket in the current project. - - ## Notes - - * Policy permissions required - * `buckets` permissions: `select` - * `objects` permissions: none - - ## Examples - - iex> Supabase.Storage.retrieve_bucket_info(client, "avatars") - {:ok, %Supabase.Storage.Bucket{...}} - - iex> Supabase.Storage.retrieve_bucket_info(invalid_conn, "avatars") - {:error, reason} - - """ - @impl true - def retrieve_bucket_info(client, id) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - BucketHandler.retrieve_info(base_url, api_key, token, id) - end - end - - @doc """ - Creates a new bucket in the current project given a map of attributes. - - ## Attributes - - * `id`: the id of the bucket to be created, required - * `name`: the name of the bucket to be created, defaults to the `id` provided - * `file_size_limit`: the maximum size of a file in bytes - * `allowed_mime_types`: a list of allowed mime types, defaults to allow all MIME types - * `public`: whether the bucket is public or not, defaults to `false` - - ## Notes - - * Policy permissions required - * `buckets` permissions: `insert` - * `objects` permissions: none - - ## Examples - - iex> Supabase.Storage.create_bucket(client, %{id: "avatars"}) - {:ok, %Supabase.Storage.Bucket{...}} - - iex> Supabase.Storage.create_bucket(invalid_conn, %{id: "avatars"}) - {:error, reason} - - """ - @impl true - def create_bucket(client, attrs) when is_client(client) do - with {:ok, bucket_params} <- Bucket.create_changeset(attrs), - %Conn{access_token: token, api_key: api_key, base_url: base_url} <- - Client.retrieve_connection(client), - {:ok, _} <- BucketHandler.create(base_url, api_key, token, bucket_params) do - retrieve_bucket_info(client, bucket_params.id) - else - nil -> - {:error, :invalid_client} - - {:error, changeset} -> - {:error, changeset} - end - end - - @doc """ - Updates a bucket in the current project given a map of attributes. - - ## Attributes - - * `file_size_limit`: the maximum size of a file in bytes - * `allowed_mime_types`: a list of allowed mime types, defaults to allow all MIME types - * `public`: whether the bucket is public or not, defaults to `false` - - Isn't possible to update a bucket's `id` or `name`. If you want or need this, you should - firstly delete the bucket and then create a new one. - - ## Notes - - * Policy permissions required - * `buckets` permissions: `update` - * `objects` permissions: none - - ## Examples - - iex> Supabase.Storage.update_bucket(client, bucket, %{public: true}) - {:ok, %Supabase.Storage.Bucket{...}} - - iex> Supabase.Storage.update_bucket(invalid_conn, bucket, %{public: true}) - {:error, reason} - - """ - @impl true - def update_bucket(client, bucket, attrs) when is_client(client) do - with {:ok, bucket_params} <- Bucket.update_changeset(bucket, attrs), - %Conn{access_token: token, api_key: api_key, base_url: base_url} <- - Client.retrieve_connection(client), - {:ok, _} <- BucketHandler.update(base_url, api_key, token, bucket.id, bucket_params) do - retrieve_bucket_info(client, bucket.id) - else - nil -> - {:error, :invalid_client} - - {:error, changeset} -> - {:error, changeset} - end - end - - @doc """ - Empties a bucket in the current project. This action deletes all objects in the bucket. - - ## Notes - - * Policy permissions required - * `buckets` permissions: `update` - * `objects` permissions: `delete` - - ## Examples - - iex> Supabase.Storage.empty_bucket(client, bucket) - {:ok, :emptied} - - iex> Supabase.Storage.empty_bucket(invalid_conn, bucket) - {:error, reason} - - """ - @impl true - def empty_bucket(client, %Bucket{} = bucket) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - BucketHandler.empty(base_url, api_key, token, bucket.id) - end - end - - @doc """ - Deletes a bucket in the current project. Notice that this also deletes all objects in the bucket. - - ## Notes - - * Policy permissions required - * `buckets` permissions: `delete` - * `objects` permissions: `delete` - - ## Examples - - iex> Supabase.Storage.delete_bucket(client, bucket) - {:ok, :deleted} - - iex> Supabase.Storage.delete_bucket(invalid_conn, bucket) - {:error, reason} - - """ - @impl true - def delete_bucket(client, %Bucket{} = bucket) when is_client(client) do - with %Conn{access_token: token, api_key: api_key, base_url: base_url} <- - Client.retrieve_connection(client), - {:ok, _} <- BucketHandler.delete(base_url, api_key, token, bucket.id) do - {:ok, :deleted} - else - nil -> - {:error, :invalid_client} - - {:error, changeset} -> - {:error, changeset} - end - end - - @doc """ - Removes an object from a bucket in the current project. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `delete` - - ## Examples - - iex> Supabase.Storage.remove_object(client, bucket, object) - {:ok, :deleted} - - iex> Supabase.Storage.remove_object(invalid_conn, bucket, object) - {:error, reason} - - """ - @impl true - def remove_object(client, %Bucket{} = bucket, %Object{} = object) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - ObjectHandler.remove(base_url, api_key, token, bucket.name, object.path) - end - end - - @doc """ - Moves a object from a bucket and send it to another bucket, in the current project. - Notice that isn't necessary to pass the current bucket, because the object already - contains this information. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `delete` and `create` - - ## Examples - - iex> Supabase.Storage.move_object(client, bucket, object) - {:ok, :moved} - - iex> Supabase.Storage.move_object(invalid_conn, bucket, object) - {:error, reason} - - """ - @impl true - def move_object(client, %Bucket{} = bucket, %Object{} = object, to) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - ObjectHandler.move(base_url, api_key, token, bucket.name, object.path, to) - end - end - - @doc """ - Copies a object from a bucket and send it to another bucket, in the current project. - Notice that isn't necessary to pass the current bucket, because the object already - contains this information. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `create` - - ## Examples - - iex> Supabase.Storage.copy_object(client, bucket, object) - {:ok, :copied} - - iex> Supabase.Storage.copy_object(invalid_conn, bucket, object) - {:error, reason} - - """ - @impl true - def copy_object(client, %Bucket{} = bucket, %Object{} = object, to) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - ObjectHandler.copy(base_url, api_key, token, bucket.name, object.path, to) - end - end - - @doc """ - Retrieves information about an object in a bucket in the current project. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `select` - - ## Examples - - iex> Supabase.Storage.retrieve_object_info(client, bucket, "some.png") - {:ok, %Supabase.Storage.Object{...}} - - iex> Supabase.Storage.retrieve_object_info(invalid_conn, bucket, "some.png") - {:error, reason} - - """ - @impl true - def retrieve_object_info(client, %Bucket{} = bucket, wildcard) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - ObjectHandler.get_info(base_url, api_key, token, bucket.name, wildcard) - end - end - - @doc """ - Lists a set of objects in a bucket in the current project. - - ## Searching - - You can pass a prefix to filter the objects returned. For example, if you have the following - objects in your bucket: - - . - └── bucket/ - ├── avatars/ - │ └── some.png - ├── other.png - └── some.pdf - - And you want to list only the objects inside the `avatars` folder, you can do: - - iex> Supabase.Storage.list_objects(client, bucket, "avatars/") - {:ok, [%Supabase.Storage.Object{...}]} - - Also you can pass some search options as a `Supabase.Storage.SearchOptions` struct. Available - options are: - - * `limit`: the maximum number of objects to return - * `offset`: the number of objects to skip - * `sort_by`: - * `column`: the column to sort by, defaults to `created_at` - * `order`: the order to sort by, defaults to `desc` - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `select` - - ## Examples - - iex> Supabase.Storage.list_objects(client, bucket) - {:ok, [%Supabase.Storage.Object{...}, ...]} - - iex> Supabase.Storage.list_objects(invalid_conn, bucket) - {:error, reason} - - """ - @impl true - def list_objects(client, %Bucket{} = bucket, prefix \\ "", opts \\ %SearchOptions{}) - when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - ObjectHandler.list(base_url, api_key, token, bucket.name, prefix, opts) - end - end - - @doc """ - Uploads a file to a bucket in the current project. Notice that you only need to - pass the path to the file you want to upload, as the file will be read in a stream way - to be sent to the server. - - ## Options - - You can pass some options as a `Supabase.Storage.ObjectOptions` struct. Available - options are: - - * `cache_control`: the cache control header value, defaults to `3600` - * `content_type`: the content type header value, defaults to `text/plain;charset=UTF-8` - * `upsert`: whether to overwrite the object if it already exists, defaults to `false` - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `insert` - - ## Examples - - iex> Supabase.Storage.upload_object(client, bucket, "avatars/some.png", "path/to/file.png") - {:ok, %Supabase.Storage.Object{...}} - - iex> Supabase.Storage.upload_object(invalid_conn, bucket, "avatars/some.png", "path/to/file.png") - {:error, reason} - - """ - @impl true - def upload_object(client, %Bucket{} = bucket, path, file, opts \\ %ObjectOptions{}) - when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - file = Path.expand(file) - ObjectHandler.create_file(base_url, api_key, token, bucket.name, path, file, opts) - end - end - - @doc """ - Downloads an object from a bucket in the current project. That return a binary that - represents the object content. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `select` - - ## Examples - - iex> Supabase.Storage.download_object(client, %Bucket{}, "avatars/some.png") - {:ok, <<>>} - - iex> Supabase.Storage.download_object(invalid_conn, %Bucket{}, "avatars/some.png") - {:error, reason} - - """ - @impl true - def download_object(client, %Bucket{} = bucket, wildcard) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - ObjectHandler.get(base_url, api_key, token, bucket.name, wildcard) - end - end - - @doc """ - Downloads an object from a bucket in the current project. That return a stream that - represents the object content. Notice that the request to the server is only made - when you start to consume the stream. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `select` - - ## Examples - - iex> Supabase.Storage.download_object_lazy(client, %Bucket{}, "avatars/some.png") - {:ok, #Function<59.128620087/2 in Stream.resource/3>} - - iex> Supabase.Storage.download_object_lazy(invalid_conn, %Bucket{}, "avatars/some.png") - {:error, reason} - - """ - @impl true - def download_object_lazy(client, %Bucket{} = bucket, wildcard) when is_client(client) do - case Client.retrieve_connection(client) do - nil -> - {:error, :invalid_client} - - %Conn{access_token: token, api_key: api_key, base_url: base_url} -> - ObjectHandler.get_lazy(base_url, api_key, token, bucket.name, wildcard) - end - end - - @doc """ - Saves an object from a bucket in the current project to a file in the local filesystem. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `select` - - ## Examples - - iex> Supabase.Storage.save_object(client, "./some.png", %Bucket{}, "avatars/some.png") - :ok - - iex> Supabase.Storage.save_object(client, "./some.png", %Bucket{}, "do_not_exist.png") - {:error, reason} - - """ - @impl true - def save_object(client, path, %Bucket{} = bucket, wildcard) when is_client(client) do - with {:ok, bin} <- download_object(client, bucket, wildcard) do - File.write(Path.expand(path), bin) - end - end - - @doc """ - Saves an object from a bucket in the current project to a file in the local filesystem. - Notice that the request to the server is only made when you start to consume the stream. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `select` - - ## Examples - - iex> Supabase.Storage.save_object_stream(client, "./some.png", %Bucket{}, "avatars/some.png") - :ok - - iex> Supabase.Storage.save_object_stream(client, "./some.png", %Bucket{}, "do_not_exist.png") - {:error, reason} - - """ - @impl true - def save_object_stream(client, path, %Bucket{} = bucket, wildcard) when is_client(client) do - with {:ok, stream} <- download_object_lazy(client, bucket, wildcard) do - fs = File.stream!(Path.expand(path)) - - stream - |> Stream.into(fs) - |> Stream.run() - end - end - - @doc """ - Creates a signed URL for an object in a bucket in the current project. This URL can - be used to perform an HTTP request to the object, without the need of authentication. - Usually this is used to allow users to download objects from a bucket. - - ## Notes - - * Policy permissions required - * `buckets` permissions: none - * `objects` permissions: `select` - - ## Examples - - iex> Supabase.Storage.create_signed_url(client, bucket, "avatars/some.png", 3600) - {:ok, "https://.supabase.co"/object/sign//?token=} - - iex> Supabase.Storage.create_signed_url(invalid_client, bucket, "avatars/some.png", 3600) - {:error, :invalid_client} - - """ - @impl true - def create_signed_url(client, %Bucket{} = bucket, path, expires_in) when is_client(client) do - with %Conn{access_token: token, api_key: api_key, base_url: base_url} <- - Client.retrieve_connection(client), - {:ok, sign_url} <- - ObjectHandler.create_signed_url( - base_url, - api_key, - token, - bucket.name, - path, - expires_in - ) do - {:ok, URI.to_string(URI.merge(base_url, sign_url))} - else - nil -> - {:error, :invalid_client} - - err -> - err - end - end -end diff --git a/lib/supabase/storage/action_error.ex b/lib/supabase/storage/action_error.ex deleted file mode 100644 index 811f371..0000000 --- a/lib/supabase/storage/action_error.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Supabase.Storage.ActionError do - @moduledoc "Represents an Error on a Supabase Storage Action" - - defexception [:message] -end diff --git a/lib/supabase/storage/bucket.ex b/lib/supabase/storage/bucket.ex deleted file mode 100644 index c362b93..0000000 --- a/lib/supabase/storage/bucket.ex +++ /dev/null @@ -1,127 +0,0 @@ -defmodule Supabase.Storage.Bucket do - @moduledoc """ - Represents a Bucket on Supabase Storage. - - This module defines the structure and operations related to a storage bucket on Supabase. - - ## Structure - - A `Bucket` consists of: - - - `id`: The unique identifier for the bucket. - - `name`: The display name of the bucket. - - `owner`: The owner of the bucket. - - `file_size_limit`: The maximum file size allowed in the bucket (in bytes). Can be `nil` for no limit. - - `allowed_mime_types`: List of MIME types permitted in this bucket. Can be `nil` for no restrictions. - - `created_at`: Timestamp indicating when the bucket was created. - - `updated_at`: Timestamp indicating the last update to the bucket. - - `public`: Boolean flag determining if the bucket is publicly accessible or not. - - ## Functions - - - `parse!/1`: Parses and returns a bucket structure. - - `create_changeset/1`: Generates a changeset for creating a bucket. - - `update_changeset/2`: Generates a changeset for updating an existing bucket. - - ## Examples - - ### Parsing a bucket - - bucket_attrs = %{ - id: "bucket_id", - name: "My Bucket", - ... - } - Supabase.Storage.Bucket.parse!(bucket_attrs) - - ### Creating a bucket changeset - - new_bucket_attrs = %{ - id: "new_bucket_id", - ... - } - Supabase.Storage.Bucket.create_changeset(new_bucket_attrs) - - ### Updating a bucket - - existing_bucket = %Supabase.Storage.Bucket{ - id: "existing_bucket_id", - ... - } - updated_attrs = %{ - public: true - } - Supabase.Storage.Bucket.update_changeset(existing_bucket, updated_attrs) - """ - - use Ecto.Schema - - import Ecto.Changeset - - @type t :: %__MODULE__{ - id: String.t(), - name: String.t(), - owner: String.t(), - file_size_limit: integer | nil, - allowed_mime_types: list(String.t()) | nil, - created_at: NaiveDateTime.t(), - updated_at: NaiveDateTime.t(), - public: boolean - } - - @fields ~w(id name created_at updated_at file_size_limit allowed_mime_types public owner)a - @create_fields ~w(id name file_size_limit allowed_mime_types public)a - @update_fields ~w(file_size_limit allowed_mime_types public)a - - @primary_key false - embedded_schema do - field(:id, :string) - field(:name, :string) - field(:owner, :string) - field(:file_size_limit, :integer) - field(:allowed_mime_types, {:array, :string}) - field(:created_at, :naive_datetime) - field(:updated_at, :naive_datetime) - field(:public, :boolean, default: false) - end - - @spec parse!(map) :: t - def parse!(attrs) do - %__MODULE__{} - |> cast(attrs, @fields) - |> apply_action!(:parse) - end - - @spec create_changeset(map) :: {:ok, map} | {:error, Ecto.Changeset.t()} - def create_changeset(attrs) do - %__MODULE__{} - |> cast(attrs, @create_fields) - |> validate_required([:id]) - |> maybe_put_name() - |> apply_action(:create) - |> case do - {:ok, data} -> {:ok, Map.take(data, @create_fields)} - err -> err - end - end - - defp maybe_put_name(changeset) do - if get_change(changeset, :name) do - changeset - else - id = get_change(changeset, :id) - put_change(changeset, :name, id) - end - end - - @spec update_changeset(t, map) :: {:ok, map} | {:error, Ecto.Changeset.t()} - def update_changeset(%__MODULE__{} = bucket, attrs) do - bucket - |> cast(attrs, @update_fields) - |> apply_action(:update) - |> case do - {:ok, data} -> {:ok, Map.take(data, @update_fields)} - err -> err - end - end -end diff --git a/lib/supabase/storage/cache.ex b/lib/supabase/storage/cache.ex deleted file mode 100644 index 6152a7d..0000000 --- a/lib/supabase/storage/cache.ex +++ /dev/null @@ -1,97 +0,0 @@ -defmodule Supabase.Storage.Cache do - @moduledoc """ - Provides caching mechanisms for Supabase Storage Buckets. - - This module acts as a GenServer that offers caching capabilities, especially for bucket-related operations in Supabase Storage. The caching is backed by the `:ets` (Erlang Term Storage) to provide in-memory storage and fast retrieval of cached data. - - ## Features - - - **Bucket Caching**: Store and retrieve buckets by their unique identifier. - - **Cache Flushing**: Clear the cache when necessary. - - **Configurable Cache Size**: Limit the number of items that can be stored in the cache. - - ## Usage - - ### Starting the Cache Server - - Supabase.Storage.Cache.start_link(%{cache_max_size: 200}) - - ### Caching Buckets - - buckets = [%{id: "bucket_1", ...}, %{id: "bucket_2", ...}] - Supabase.Storage.Cache.cache_buckets(buckets) - - ### Retrieving a Cached Bucket by ID - - Supabase.Storage.Cache.find_bucket_by_id("bucket_1") - - ### Clearing the Cache - - Supabase.Storage.Cache.flush() - - ## Implementation Details - - The cache uses the `:ets` module for in-memory storage of buckets. The number of buckets cached is controlled by the `:cache_max_size` option (default: 100). When the cache is close to exceeding its maximum size, older entries are removed to accommodate new ones. - """ - - use GenServer - - ## Client - - def start_link(args) do - GenServer.start_link(__MODULE__, args, name: __MODULE__) - end - - def find_bucket_by_id(id) do - GenServer.call(__MODULE__, {:find_bucket, id: id}) - end - - def cache_buckets(buckets) do - GenServer.cast(__MODULE__, {:cache_buckets, buckets}) - end - - def flush do - GenServer.cast(__MODULE__, :flush) - end - - ## API - - @impl true - def init(args) do - Process.flag(:trap_exit, true) - table = :ets.new(:buckets_cache, [:set, :public, :named_table]) - max_size = Keyword.get(args, :cache_max_size, 100) - {:ok, %{table: table, max_size: max_size, size: 0}} - end - - @impl true - def handle_cast(:flush, table) do - :ets.delete_all_objects(table) - {:noreply, table} - end - - def handle_cast({:cache_buckets, buckets}, state) do - if overflowed_max_size?(state, buckets) do - :ets.delete_all_objects(state.table) - end - - # prefer atomic operations - for bucket <- buckets do - :ets.insert_new(state.table, {bucket.id, bucket}) - end - - {:noreply, %{state | size: length(buckets)}} - end - - defp overflowed_max_size?(state, buckets) do - state.size + length(buckets) > state.max_size - end - - @impl true - def handle_call({:find_bucket, id: id}, _from, state) do - bucket = :ets.lookup_element(state.table, id, 2) - {:reply, bucket, state} - rescue - _ -> {:reply, nil, state} - end -end diff --git a/lib/supabase/storage/cache_reloader.ex b/lib/supabase/storage/cache_reloader.ex deleted file mode 100644 index 137ade1..0000000 --- a/lib/supabase/storage/cache_reloader.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule Supabase.Storage.CacheReloader do - @moduledoc """ - Periodically reloads and updates the bucket cache for Supabase Storage. - - This module acts as a GenServer that schedules periodic tasks to reload and update the cache for Supabase Storage Buckets. It collaborates with the `Supabase.Storage.Cache` to ensure that the cached data remains fresh and updated. - - ## Features - - - **Automatic Cache Reloading**: Periodically reloads the buckets from Supabase Storage and updates the cache. - - **Configurable Reload Interval**: The time interval between successive cache reloads can be specified. - - ## Usage - - ### Starting the CacheReloader Server - - Supabase.Storage.CacheReloader.start_link(%{reload_interval: 2_000}) - - ## Implementation Details - - By default, the reload interval is set to 1 second (`@ttl`). This means the cache will be updated every second with the latest data from Supabase Storage. This interval can be configured during the server start using the `:reload_interval` option. - - The server interacts with `Supabase.Storage.list_buckets/1` to fetch the list of buckets and then updates the cache using `Supabase.Storage.Cache.cache_buckets/1`. - """ - - use GenServer - - alias Supabase.Storage.Cache - - # @ttl 60_000 - @ttl 1_000 - - def start_link(args) do - GenServer.start_link(__MODULE__, args, name: __MODULE__) - end - - @impl true - def init(args) do - Process.flag(:trap_exit, true) - interval = Keyword.get(args, :reload_interval, @ttl) - Process.send_after(self(), :reload, interval) - {:ok, interval} - end - - @impl true - def handle_info(:reload, interval) do - {:ok, buckets} = Supabase.Storage.list_buckets(Supabase.Connection) - :ok = Cache.cache_buckets(buckets) - Process.send_after(self(), :reload, interval) - {:noreply, interval} - end -end diff --git a/lib/supabase/storage/endpoints.ex b/lib/supabase/storage/endpoints.ex deleted file mode 100644 index f3271bf..0000000 --- a/lib/supabase/storage/endpoints.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule Supabase.Storage.Endpoints do - @moduledoc "Defines the Endpoints for the Supabase Storage API" - - def bucket_path do - "/storage/v1/bucket" - end - - def bucket_path_with_id(id) do - "/storage/v1/bucket/#{id}" - end - - def bucket_path_to_empty(id) do - bucket_path_with_id(id) <> "/empty" - end - - def file_upload_url(path) do - "/storage/v1/object/upload/sign/#{path}" - end - - def file_move do - "/storage/v1/object/move" - end - - def file_copy do - "/storage/v1/object/copy" - end - - def file_upload(bucket, path) do - "/storage/v1/object/#{bucket}/#{path}" - end - - def file_info(bucket, wildcard) do - "/storage/v1/object/info/authenticated/#{bucket}/#{wildcard}" - end - - def file_list(bucket) do - "/storage/v1/object/list/#{bucket}" - end - - def file_remove(bucket) do - "/storage/v1/object/#{bucket}" - end - - def file_signed_url(bucket, path) do - "/storage/v1/object/sign/#{bucket}/#{path}" - end - - def file_download(bucket, wildcard) do - "/storage/v1/object/authenticated/#{bucket}/#{wildcard}" - end -end diff --git a/lib/supabase/storage/handlers/bucket_handler.ex b/lib/supabase/storage/handlers/bucket_handler.ex deleted file mode 100644 index 02dbe0e..0000000 --- a/lib/supabase/storage/handlers/bucket_handler.ex +++ /dev/null @@ -1,133 +0,0 @@ -defmodule Supabase.Storage.BucketHandler do - @moduledoc """ - Provides low-level API functions for managing Supabase Storage buckets. - - The `BucketHandler` module offers a collection of functions that directly interact with the Supabase Storage API for managing buckets. This module works closely with the `Supabase.Fetcher` for sending HTTP requests and the `Supabase.Storage.Cache` for caching bucket information. - - ## Features - - - **Bucket Listing**: Fetch a list of all the buckets available in the storage. - - **Bucket Retrieval**: Retrieve detailed information about a specific bucket. - - **Bucket Creation**: Create a new bucket with specified attributes. - - **Bucket Update**: Modify the attributes of an existing bucket. - - **Bucket Emptying**: Empty the contents of a bucket without deleting the bucket itself. - - **Bucket Deletion**: Permanently remove a bucket and its contents. - - ## Caution - - This module provides a low-level interface to Supabase Storage buckets and is designed for internal use by the `Supabase.Storage` module. Direct use is discouraged unless you need to perform custom or unsupported actions that are not available through the higher-level API. Incorrect use can lead to unexpected results or data loss. - - ## Implementation Details - - All functions within the module expect a base URL, API key, and access token as their initial arguments, followed by any additional arguments required for the specific operation. Responses are usually in the form of `{:ok, result}` or `{:error, message}` tuples. - """ - - alias Supabase.Connection, as: Conn - alias Supabase.Fetcher - alias Supabase.Storage.Bucket - alias Supabase.Storage.Cache - alias Supabase.Storage.Endpoints - - @type bucket_id :: String.t() - @type bucket_name :: String.t() - @type create_attrs :: %{ - id: String.t(), - name: String.t(), - file_size_limit: integer | nil, - allowed_mime_types: list(String.t()) | nil, - public: boolean - } - @type update_attrs :: %{ - public: boolean | nil, - file_size_limit: integer | nil, - allowed_mime_types: list(String.t()) | nil - } - - @spec list(Conn.base_url(), Conn.api_key(), Conn.access_token()) :: - {:ok, [Bucket.t()]} | {:error, String.t()} - def list(base_url, api_key, token) do - url = Fetcher.get_full_url(base_url, Endpoints.bucket_path()) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.get(headers) - |> case do - {:ok, body} -> {:ok, Enum.map(body, &Bucket.parse!/1)} - {:error, msg} -> {:error, msg} - end - end - - @spec retrieve_info(Conn.base_url(), Conn.api_key(), Conn.access_token(), bucket_id) :: - {:ok, Bucket.t()} | {:error, String.t()} - def retrieve_info(base_url, api_key, token, bucket_id) do - if bucket = Cache.find_bucket_by_id(bucket_id) do - {:ok, bucket} - else - url = Fetcher.get_full_url(base_url, Endpoints.bucket_path_with_id(bucket_id)) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.get(headers) - |> case do - {:ok, body} -> {:ok, Bucket.parse!(body)} - {:error, msg} -> {:error, msg} - end - end - end - - @spec create(Conn.base_url(), Conn.api_key(), Conn.access_token(), create_attrs) :: - {:ok, Bucket.t()} | {:error, String.t()} - def create(base_url, api_key, token, attrs) do - url = Fetcher.get_full_url(base_url, Endpoints.bucket_path()) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.post(attrs, headers) - |> case do - {:ok, resp} -> {:ok, resp} - {:error, msg} -> {:error, msg} - end - end - - @spec update(Conn.base_url(), Conn.api_key(), Conn.access_token(), bucket_id, update_attrs) :: - {:ok, Bucket.t()} | {:error, String.t()} - def update(base_url, api_key, token, id, attrs) do - url = Fetcher.get_full_url(base_url, Endpoints.bucket_path_with_id(id)) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.put(attrs, headers) - |> case do - {:ok, message} -> {:ok, message} - {:error, msg} -> {:error, msg} - end - end - - @spec empty(Conn.base_url(), Conn.api_key(), Conn.access_token(), bucket_id) :: - {:ok, :successfully_emptied} | {:error, String.t()} - def empty(base_url, api_key, token, id) do - url = Fetcher.get_full_url(base_url, Endpoints.bucket_path_to_empty(id)) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.post(nil, headers) - |> case do - {:ok, _message} -> {:ok, :successfully_emptied} - {:error, msg} -> {:error, msg} - end - end - - @spec delete(Conn.base_url(), Conn.api_key(), Conn.access_token(), bucket_id) :: - {:ok, String.t()} | {:error, String.t()} - def delete(base_url, api_key, token, id) do - url = Fetcher.get_full_url(base_url, Endpoints.bucket_path_with_id(id)) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.delete(nil, headers) - |> case do - {:ok, body} -> {:ok, body} - {:error, msg} -> {:error, msg} - end - end -end diff --git a/lib/supabase/storage/handlers/object_handler.ex b/lib/supabase/storage/handlers/object_handler.ex deleted file mode 100644 index 111eda0..0000000 --- a/lib/supabase/storage/handlers/object_handler.ex +++ /dev/null @@ -1,226 +0,0 @@ -defmodule Supabase.Storage.ObjectHandler do - @moduledoc """ - A low-level API interface for managing objects within a Supabase bucket. - - ## Responsibilities - - - **File Management**: Create, move, copy, and get information about files in a bucket. - - **Object Listing**: List objects based on certain criteria, like a prefix. - - **Object Removal**: Delete specific objects or a list of objects. - - **URL Management**: Generate signed URLs for granting temporary access to objects. - - **Content Access**: Retrieve the content of an object or stream it. - - ## Usage Warning - - This module is meant for internal use or for developers requiring more control over object management in Supabase. In general, users should work with the higher-level Supabase.Storage API when possible, as it may offer better abstractions and safety mechanisms. - - Directly interfacing with this module bypasses any additional logic the main API might provide. Use it with caution and ensure you understand its operations. - """ - - alias Supabase.Connection, as: Conn - alias Supabase.Fetcher - alias Supabase.Storage.Endpoints - alias Supabase.Storage.Object - alias Supabase.Storage.ObjectOptions, as: Opts - alias Supabase.Storage.SearchOptions, as: Search - - @type bucket_name :: String.t() - @type object_path :: Path.t() - @type file_path :: Path.t() - @type opts :: Opts.t() - @type search_opts :: Search.t() - @type wildcard :: String.t() - @type prefix :: String.t() - - @spec create_file( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - object_path, - file_path, - opts - ) :: - {:ok, Object.t()} | {:error, String.t()} - def create_file(url, api_key, token, bucket, object_path, file_path, %Opts{} = opts) do - url = Fetcher.get_full_url(url, Endpoints.file_upload(bucket, object_path)) - - headers = - Fetcher.apply_headers(api_key, token, [ - {"cache-control", "max-age=#{opts.cache_control}"}, - {"content-type", opts.content_type}, - {"x-upsert", to_string(opts.upsert)} - ]) - - Fetcher.upload(:post, url, file_path, headers) - rescue - File.Error -> {:error, :file_not_found} - end - - @spec move( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - object_path, - object_path - ) :: - {:ok, :moved} | {:error, String.t()} - def move(base_url, api_key, token, bucket_id, path, to) do - url = Fetcher.get_full_url(base_url, Endpoints.file_move()) - headers = Fetcher.apply_headers(api_key, token) - body = %{bucket_id: bucket_id, source_key: path, destination_key: to} - - url - |> Fetcher.post(body, headers) - |> case do - {:ok, _} -> {:ok, :moved} - {:error, msg} -> {:error, msg} - end - end - - @spec copy( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - object_path, - object_path - ) :: - {:ok, :copied} | {:error, String.t()} - def copy(base_url, api_key, token, bucket_id, path, to) do - url = Fetcher.get_full_url(base_url, Endpoints.file_copy()) - headers = Fetcher.apply_headers(api_key, token) - body = %{bucket_id: bucket_id, source_key: path, destination_key: to} - - url - |> Fetcher.post(body, headers) - |> case do - {:ok, _} -> {:ok, :copied} - {:error, msg} -> {:error, msg} - end - end - - @spec get_info( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - wildcard - ) :: - {:ok, Object.t()} | {:error, String.t()} - def get_info(base_url, api_key, token, bucket_name, wildcard) do - url = Fetcher.get_full_url(base_url, Endpoints.file_info(bucket_name, wildcard)) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.get(headers) - |> case do - {:ok, data} -> {:ok, Object.parse!(data)} - {:error, msg} -> {:error, msg} - end - end - - @spec list( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - prefix, - search_opts - ) :: - {:ok, [Object.t()]} | {:error, String.t()} - def list(base_url, api_key, token, bucket_name, prefix, %Search{} = opts) do - url = Fetcher.get_full_url(base_url, Endpoints.file_list(bucket_name)) - headers = Fetcher.apply_headers(api_key, token) - body = Map.merge(%{prefix: prefix}, Map.from_struct(opts)) - - url - |> Fetcher.post(body, headers) - |> case do - {:ok, data} -> {:ok, Enum.map(data, &Object.parse!/1)} - {:error, msg} -> {:error, msg} - end - end - - @spec remove( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - object_path - ) :: - {:ok, :deleted} | {:error, String.t()} - def remove(base_url, api_key, token, bucket_name, path) do - remove_list(base_url, api_key, token, bucket_name, [path]) - end - - @spec remove_list( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - list(object_path) - ) :: - {:ok, :deleted} | {:error, String.t()} - def remove_list(base_url, api_key, token, bucket_name, paths) do - url = Fetcher.get_full_url(base_url, Endpoints.file_remove(bucket_name)) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.delete(%{prefixes: paths}, headers) - |> case do - {:ok, _} -> {:ok, :deleted} - {:error, msg} -> {:error, msg} - end - end - - @spec create_signed_url( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - object_path, - integer - ) :: - {:ok, String.t()} | {:error, String.t()} - def create_signed_url(base_url, api_key, token, bucket_name, path, expires_in) do - url = Fetcher.get_full_url(base_url, Endpoints.file_signed_url(bucket_name, path)) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.post(%{expiresIn: expires_in}, headers) - |> case do - {:ok, data} -> {:ok, data["signedURL"]} - {:error, msg} -> {:error, msg} - end - end - - @spec get(Conn.base_url(), Conn.api_key(), Conn.access_token(), bucket_name, object_path) :: - {:ok, binary} | {:error, String.t()} - def get(base_url, api_key, token, bucket_name, wildcard) do - url = Fetcher.get_full_url(base_url, Endpoints.file_download(bucket_name, wildcard)) - headers = Fetcher.apply_headers(api_key, token) - - url - |> Fetcher.get(headers) - |> case do - {:ok, data} -> {:ok, data} - {:error, msg} -> {:error, msg} - end - end - - @spec get_lazy( - Conn.base_url(), - Conn.api_key(), - Conn.access_token(), - bucket_name, - wildcard - ) :: - {:ok, Stream.t()} | {:error, atom} - def get_lazy(base_url, api_key, token, bucket_name, wildcard) do - url = Fetcher.get_full_url(base_url, Endpoints.file_download(bucket_name, wildcard)) - headers = Fetcher.apply_headers(api_key, token) - Fetcher.stream(url, headers) - end -end diff --git a/lib/supabase/storage/object.ex b/lib/supabase/storage/object.ex deleted file mode 100644 index 3ca142d..0000000 --- a/lib/supabase/storage/object.ex +++ /dev/null @@ -1,75 +0,0 @@ -defmodule Supabase.Storage.Object do - @moduledoc """ - Represents an Object within a Supabase Storage Bucket. - - This module encapsulates the structure and operations related to an object or file stored within a Supabase Storage bucket. - - ## Structure - - An `Object` has the following attributes: - - - `id`: The unique identifier for the object. - - `path`: The path to the object within its storage bucket. - - `bucket_id`: The ID of the bucket that houses this object. - - `name`: The name or title of the object. - - `owner`: The owner or uploader of the object. - - `metadata`: A map containing meta-information about the object (e.g., file type, size). - - `created_at`: Timestamp indicating when the object was first uploaded or created. - - `updated_at`: Timestamp indicating the last time the object was updated. - - `last_accessed_at`: Timestamp of when the object was last accessed or retrieved. - - ## Functions - - - `parse!/1`: Accepts a map of attributes and constructs a structured `Object`. - - ## Examples - - ### Parsing an object - - object_attrs = %{ - id: "obj_id", - path: "/folder/my_file.txt", - bucket_id: "bucket_123", - ... - } - Supabase.Storage.Object.parse!(object_attrs) - """ - - use Ecto.Schema - - import Ecto.Changeset, only: [cast: 3, apply_action!: 2] - - @type t :: %__MODULE__{ - id: String.t(), - path: Path.t(), - bucket_id: String.t(), - name: String.t(), - owner: String.t(), - metadata: map(), - created_at: NaiveDateTime.t(), - updated_at: NaiveDateTime.t(), - last_accessed_at: NaiveDateTime.t() - } - - @fields ~w(id path bucket_id name owner created_at updated_at metadata last_accessed_at)a - - @primary_key false - embedded_schema do - field(:path, :string) - field(:id, :string) - field(:bucket_id, :string) - field(:name, :string) - field(:owner, :string) - field(:metadata, :map) - field(:created_at, :naive_datetime) - field(:updated_at, :naive_datetime) - field(:last_accessed_at, :naive_datetime) - end - - @spec parse!(map) :: t - def parse!(attrs) do - %__MODULE__{} - |> cast(attrs, @fields) - |> apply_action!(:parse) - end -end diff --git a/lib/supabase/storage/object_options.ex b/lib/supabase/storage/object_options.ex deleted file mode 100644 index 253c506..0000000 --- a/lib/supabase/storage/object_options.ex +++ /dev/null @@ -1,57 +0,0 @@ -defmodule Supabase.Storage.ObjectOptions do - @moduledoc """ - Represents the configurable options for an Object within Supabase Storage. - - This module encapsulates options that can be set or modified for a storage object. These options help in controlling behavior such as caching, content type, and whether to upsert an object. - - ## Structure - - An `ObjectOptions` consists of the following attributes: - - - `cache_control`: Specifies directives for caching mechanisms in both requests and responses. Default is `"3600"`. - - `content_type`: Specifies the media type of the resource or data. Default is `"text/plain;charset=UTF-8"`. - - `upsert`: A boolean that, when set to `true`, will insert the object if it does not exist, or update it if it does. Default is `true`. - - ## Functions - - - `parse!/1`: Accepts a map of attributes and constructs a structured `ObjectOptions`. - - ## Examples - - ### Parsing object options - - options_attrs = %{ - cache_control: "no-cache", - content_type: "application/json", - upsert: false - } - Supabase.Storage.ObjectOptions.parse!(options_attrs) - """ - - use Ecto.Schema - - import Ecto.Changeset, only: [cast: 3, apply_action!: 2] - - @type t :: %__MODULE__{ - cache_control: String.t(), - content_type: String.t(), - upsert: boolean() - } - - @fields ~w(cache_control content_type upsert)a - - @derive Jason.Encoder - @primary_key false - embedded_schema do - field(:cache_control, :string, default: "3600") - field(:content_type, :string, default: "text/plain;charset=UTF-8") - field(:upsert, :boolean, default: true) - end - - @spec parse!(map) :: t - def parse!(attrs) do - %__MODULE__{} - |> cast(attrs, @fields) - |> apply_action!(:parse) - end -end diff --git a/lib/supabase/storage/search_options.ex b/lib/supabase/storage/search_options.ex deleted file mode 100644 index 25f8339..0000000 --- a/lib/supabase/storage/search_options.ex +++ /dev/null @@ -1,60 +0,0 @@ -defmodule Supabase.Storage.SearchOptions do - @moduledoc """ - Represents the search options for querying objects within Supabase Storage. - - This module encapsulates various options that aid in fetching and sorting storage objects. These options include specifying the limit on the number of results, an offset for pagination, and a sorting directive. - - ## Structure - - A `SearchOptions` consists of the following attributes: - - - `limit`: Specifies the maximum number of results to return. Default is `100`. - - `offset`: Specifies the number of results to skip before starting to fetch the result set. Useful for implementing pagination. Default is `0`. - - `sort_by`: A map that provides a sorting directive. It defines which column should be used for sorting and the order (ascending or descending). Default is `%{column: "name", order: "asc"}`. - - ## Functions - - - `parse!/1`: Accepts a map of attributes and constructs a structured `SearchOptions`. - - ## Examples - - ### Parsing search options - - search_attrs = %{ - limit: 50, - offset: 10, - sort_by: %{column: "created_at", order: "desc"} - } - Supabase.Storage.SearchOptions.parse!(search_attrs) - """ - - use Ecto.Schema - - import Ecto.Changeset, only: [cast: 3, apply_action!: 2] - - @type t :: %__MODULE__{ - limit: integer(), - offset: integer(), - sort_by: %{ - column: String.t(), - order: String.t() - } - } - - @fields ~w(limit offset sort_by)a - - @primary_key false - @derive Jason.Encoder - embedded_schema do - field(:limit, :integer, default: 100) - field(:offset, :integer, default: 0) - field(:sort_by, :map, default: %{column: "name", order: "asc"}) - end - - @spec parse!(map) :: t - def parse!(attrs) do - %__MODULE__{} - |> cast(attrs, @fields) - |> apply_action!(:parse) - end -end diff --git a/lib/supabase/storage_behaviour.ex b/lib/supabase/storage_behaviour.ex deleted file mode 100644 index 7dee4fb..0000000 --- a/lib/supabase/storage_behaviour.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule Supabase.StorageBehaviour do - @moduledoc "Defines Supabase Storage Client callbacks" - - alias Supabase.Storage.Bucket - alias Supabase.Storage.Object - alias Supabase.Storage.ObjectOptions, as: Opts - alias Supabase.Storage.SearchOptions, as: Search - - @type conn :: atom - @type reason :: String.t() | atom - @type result(a) :: {:ok, a} | {:error, reason} | {:error, :invalid_client} - - @callback list_buckets(conn) :: result([Bucket.t()]) - @callback retrieve_bucket_info(conn, id) :: result(Bucket.t()) - when id: String.t() - @callback create_bucket(conn, map) :: result(Bucket.t()) - @callback update_bucket(conn, Bucket.t(), map) :: result(Bucket.t()) - @callback empty_bucket(conn, Bucket.t()) :: result(:emptied) - @callback delete_bucket(conn, Bucket.t()) :: result(:deleted) - - @callback remove_object(conn, Bucket.t(), Object.t()) :: result(:deleted) - @callback move_object(conn, Bucket.t(), Object.t(), String.t()) :: result(:moved) - @callback copy_object(conn, Bucket.t(), Object.t(), String.t()) :: result(:copied) - @callback retrieve_object_info(conn, Bucket.t(), String.t()) :: result(Object.t()) - @callback list_objects(conn, Bucket.t(), prefix, Search.t()) :: result([Object.t()]) - when prefix: String.t() - @callback upload_object(conn, Bucket.t(), dest, source, Opts.t()) :: result(Object.t()) - when dest: String.t(), - source: Path.t() - @callback download_object(conn, Bucket.t(), wildcard) :: result(binary) - when wildcard: String.t() - @callback download_object_lazy(conn, Bucket.t(), wildcard) :: result(Stream.t()) - when wildcard: String.t() - @callback save_object(conn, dest, Bucket.t(), wildcard) :: - :ok | {:error, atom} | {:error, :invalid_client} - when wildcard: String.t(), - dest: Path.t() - @callback save_object_stream(conn, dest, Bucket.t(), wildcard) :: - :ok | {:error, atom} | {:error, :invalid_client} - when wildcard: String.t(), - dest: Path.t() - @callback create_signed_url(conn, Bucket.t(), String.t(), integer) :: result(String.t()) -end diff --git a/mix.exs b/mix.exs index a922188..9ad3a69 100644 --- a/mix.exs +++ b/mix.exs @@ -1,12 +1,12 @@ defmodule Supabase.MixProject do use Mix.Project - @version "0.2.1" + @version "0.2.2" @source_url "https://github.com/zoedsoupe/supabase" def project do [ - app: :supabase, + app: :supabase_potion, version: @version, elixir: "~> 1.14", start_permanent: Mix.env() == :prod, @@ -37,7 +37,6 @@ defmodule Supabase.MixProject do defp package do %{ - name: "supabase_potion", licenses: ["MIT"], contributors: ["zoedsoupe"], links: %{ diff --git a/mix.lock b/mix.lock index bf0aeec..321fa40 100644 --- a/mix.lock +++ b/mix.lock @@ -2,10 +2,12 @@ "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, "earmark_parser": {:hex, :earmark_parser, "1.4.35", "437773ca9384edf69830e26e9e7b2e0d22d2596c4a6b17094a3b29f01ea65bb8", [:mix], [], "hexpm", "8652ba3cb85608d0d7aa2d21b45c6fad4ddc9a1f9a1f1b30ca3a246f0acc33f6"}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, @@ -21,6 +23,7 @@ "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"}, "req": {:git, "https://github.com/wojtekmach/req.git", "43738340748942762932dcc5a7620d3d5f09bf09", []}, "supabase_connection": {:hex, :supabase_connection, "0.1.0", "21798a8637d344b8e48471add553a02371814b599f7ce555a00d17583a5ea5a9", [:mix], [{:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:supabase_types, "~> 0.1", [hex: :supabase_types, repo: "hexpm", optional: false]}], "hexpm", "5435f2892d13c5f00d26b4a61b3fc823683fc6699936d9b7c201ebf73c33e226"}, "supabase_fetcher": {:hex, :supabase_fetcher, "0.1.0", "c2c42084306655b3cd3f747f60deacbe008395f129d937fc8c7ce13de2a8f85f", [:mix], [{:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "33725892d1fb51c4d6aca49a6142fa0ef433fd5f20a17113c469f80e090d5c5f"},