Current section

Files

Jump to
baobab lib baobab.ex
Raw

lib/baobab.ex

defmodule Baobab do
@moduledoc """
Baobab is a pure Elixir implementation of the
[Bamboo](https://github.com/AljoschaMeyer/bamboo) append-only log.
It is fairly opinionated about the filesystem persistence of the logs.
They are considered to be a spool of the logs as retreived.
Consumers of this library may wish to place a local copy of the logs in
a store with better indexing and query properties.
### Configuration
config :baobab, spool_dir: "/tmp"
### Options
- `format`: `:entry` or `:binary`, default: `:entry`
- `log_id`: the author's log identifier, default `0`
- `revalidate`: confirm the store contents are unchanged, default: `false`
"""
BaseX.prepare_module(
"Base62",
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
32
)
defp parse_options(opts) do
{Keyword.get(opts, :format, :entry), Keyword.get(opts, :log_id, 0),
Keyword.get(opts, :revalidate, false)}
end
@doc """
Create and store a new log entry for a stored identity
"""
def append_log(payload, identity, options \\ []) do
{_, log_id, _} = parse_options(options)
Baobab.Entry.create(payload, identity, log_id)
end
@doc """
Import and store a log entry from its binary format.
"""
def import(binary) do
binary |> Baobab.Entry.from_binary(false) |> Baobab.Entry.store()
end
@doc """
Retrieve the latest entry.
Includes the available certificate pool for its verification.
"""
def latest_log(author, options \\ [])
def latest_log(author, options) when byte_size(author) != 32,
do: latest_log(identity_key(author, :public), options)
def latest_log(author, options),
do: log_at(author, max_seqnum(author, options), options)
@doc """
Retrieve an author log at a particular sequence number.
Includes the available certificate pool for its verification.
"""
def log_at(author, seq, options \\ []) do
opts = parse_options(options)
certificate_pool(author, seq, opts)
|> Enum.reverse()
|> Enum.map(fn n -> Baobab.Entry.retrieve(author, n, opts) end)
end
@doc """
Retrieve all available entries in a particular log
"""
def full_log(author, options \\ [])
def full_log(author, options) when byte_size(author) != 32,
do: full_log(identity_key(author, :public), options)
def full_log(author, options) do
opts = parse_options(options)
gather_all_entries(author, opts, max_seqnum(author, options), [])
end
defp gather_all_entries(_, _, 0, acc), do: acc
defp gather_all_entries(author, opts, n, acc) do
newacc =
case Baobab.Entry.retrieve(author, n, opts) do
:error -> acc
entry -> [entry | acc]
end
gather_all_entries(author, opts, n - 1, newacc)
end
@doc false
def certificate_pool(author, seq, {_, log_id, _}) do
max = max_seqnum(author, log_id: log_id)
seq |> Lipmaa.cert_pool() |> Enum.reject(fn n -> n > max end)
end
@doc """
Retrieve the latest sequence number on a particular log identified by the
author key and log number
"""
def max_seqnum(author, options \\ [])
def max_seqnum(author, options) when byte_size(author) != 32,
do: max_seqnum(identity_key(author, :public), options)
def max_seqnum(author, options) do
a = BaseX.Base62.encode(author)
{_, log_id, _} = parse_options(options)
[log_dir(a, log_id), "**", "{entry_*}"]
|> Path.join()
|> Path.wildcard()
|> Enum.map(fn n -> Path.basename(n) end)
|> Enum.reduce(0, fn n, a ->
Enum.max([a, n |> String.split("_") |> List.last() |> String.to_integer()])
end)
end
@doc """
Retrieve the latest entry on a particular log identified by the
author key and log number
"""
def max_entry(author, options \\ [])
def max_entry(author, options) when byte_size(author) != 32,
do: max_entry(identity_key(author, :public), options)
def max_entry(author, options) do
opts = parse_options(options)
Baobab.Entry.retrieve(author, max_seqnum(author, options), opts)
end
@doc """
Create and store a new identity
"""
# Maybe make it possible to provide secret ket or both
# No overwiting? Error handling?
def create_identity(identity) do
{secret, public} = Ed25519.generate_key_pair()
where = id_dir(identity)
File.mkdir_p(where)
File.write!(Path.join([where, "secret"]), secret)
File.write!(Path.join([where, "public"]), public)
BaseX.Base62.encode(public)
end
@doc """
Retrieve the key for a stored identity.
Can be either the `:public` or `:secret` key
"""
def identity_key(identity, which) do
{:ok, key} = Path.join([id_dir(identity), Atom.to_string(which)]) |> File.read()
key
end
@doc false
def log_dir(author, log_id) when is_integer(log_id),
do: log_dir(author, Integer.to_string(log_id))
def log_dir(author, log_id) do
Path.join([proper_config_path(), "content", author, log_id])
end
defp id_dir(identity),
do: Path.join([proper_config_path(), "identity", identity])
defp proper_config_path do
Application.fetch_env!(:baobab, :spool_dir) |> Path.expand()
end
end