Packages
baobab
0.1.0
0.35.1
0.35.0
0.34.0
0.31.0
0.30.0
0.21.2
0.21.1
0.21.0
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.1
0.17.0
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.2
0.15.1
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.0
Bamboo append-only logs
Current section
Files
Jump to
Current section
Files
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