SkillKit.Storage behaviour (SkillKit v0.1.0)

Copy Markdown View Source

Behaviour and convenience API for pluggable storage backends.

All file I/O in SkillKit routes through this module, allowing the storage layer to be swapped between local filesystem, cloud object stores, or in-memory backends for testing.

Configuration

Set the provider in application config:

# config/config.exs
config :skill_kit, SkillKit.Storage,
  provider: SkillKit.Storage.File

# config/test.exs
config :skill_kit, SkillKit.Storage,
  provider: SkillKit.Storage.Memory

Using the Convenience API

Call functions directly on SkillKit.Storage — the configured provider is resolved automatically:

Storage.put!("skills/greet/SKILL.md", content)
{:ok, data} = Storage.read("skills/greet/SKILL.md")
{:ok, entries} = Storage.list("skills")
Storage.delete("skills/greet/SKILL.md")

Implementing a Custom Provider

A provider implements this behaviour's 9 callbacks. For example, an S3-backed provider:

defmodule MyApp.Storage.S3 do
  @behaviour SkillKit.Storage

  @impl true
  def read(path) do
    case ExAws.S3.get_object("my-bucket", path) |> ExAws.request() do
      {:ok, %{body: body}} -> {:ok, body}
      {:error, reason} -> {:error, reason}
    end
  end

  @impl true
  def put(path, content) do
    case ExAws.S3.put_object("my-bucket", path, content) |> ExAws.request() do
      {:ok, _} -> :ok
      {:error, reason} -> {:error, reason}
    end
  end

  @impl true
  def list(path) do
    stream =
      ExAws.S3.list_objects("my-bucket", prefix: path <> "/", delimiter: "/")
      |> ExAws.stream!()
      |> Stream.map(& &1.key)
      |> Stream.map(&Path.basename/1)

    {:ok, stream}
  end

  @impl true
  def exists?(path) do
    case ExAws.S3.head_object("my-bucket", path) |> ExAws.request() do
      {:ok, _} -> true
      {:error, _} -> false
    end
  end

  @impl true
  def dir?(path) do
    case list(path) do
      {:ok, entries} -> Enum.any?(entries)
      {:error, _} -> false
    end
  end

  @impl true
  def ensure_dir(_path), do: :ok

  @impl true
  def delete(path) do
    case ExAws.S3.delete_object("my-bucket", path) |> ExAws.request() do
      {:ok, _} -> :ok
      {:error, reason} -> {:error, reason}
    end
  end

  @impl true
  def delete_all(path) do
    {:ok, entries} = list(path)
    deleted = Enum.map(entries, fn key ->
      full = Path.join(path, key)
      delete(full)
      full
    end)
    {:ok, deleted}
  end

  @impl true
  def delete_dir(_path), do: :ok
end

Then configure it:

config :skill_kit, SkillKit.Storage,
  provider: MyApp.Storage.S3

Callback Naming

Callbacks use storage-agnostic names rather than mirroring Elixir's File module:

CallbackFile equivalentNotes
read/1File.read/1
put/2File.write/2S3 PUT semantics
delete/1File.rm/1Single file/key
delete_all/1File.rm_rf/1Recursive delete
list/1File.ls/1Returns Enumerable.t() for pagination
exists?/1File.exists?/1
dir?/1File.dir?/1S3 checks key prefix
ensure_dir/1File.mkdir_p/1No-op for flat stores
delete_dir/1File.rmdir/1Remove empty directory

Bang Wrappers

Three bang functions are provided for operations commonly used in contexts where failure should raise:

  • put!/2 — raises on write failure
  • ensure_dir!/1 — raises on directory creation failure
  • delete_all!/1 — raises on failure, returns list of removed paths on success

Summary

Types

path()

@type path() :: String.t()

Callbacks

delete(path)

@callback delete(path()) :: :ok | {:error, term()}

delete_all(path)

@callback delete_all(path()) :: {:ok, [String.t()]} | {:error, term()}

delete_dir(path)

@callback delete_dir(path()) :: :ok | {:error, term()}

dir?(path)

@callback dir?(path()) :: boolean()

ensure_dir(path)

@callback ensure_dir(path()) :: :ok | {:error, term()}

exists?(path)

@callback exists?(path()) :: boolean()

list(path)

@callback list(path()) :: {:ok, Enumerable.t()} | {:error, term()}

put(path, iodata)

@callback put(path(), iodata()) :: :ok | {:error, term()}

read(path)

@callback read(path()) :: {:ok, binary()} | {:error, term()}

Functions

delete(path)

delete_all(path)

delete_all!(path)

delete_dir(path)

dir?(path)

ensure_dir(path)

ensure_dir!(path)

exists?(path)

list(path)

put(path, content)

put!(path, content)

read(path)