defmodule Baobab.Persistence do
alias Baobab.{Entry, Identity}
@moduledoc """
Functions related to Baobab values persistence
"""
@doc """
Interact with a Baobab persistence mechanism
Actions closely mirror the underlying `dets` at present
"""
def action(which, clump_id, action, value \\ nil) do
store(which, clump_id, :open)
retval = perform_action(which, action, value)
case action in [:truncate, :delete, :put, :match_delete] do
true -> recompute_hash(clump_id, which)
false -> :ok
end
store(which, clump_id, :close)
retval
end
defp perform_action(which, :get, key) do
case :dets.lookup(which, key) do
[{^key, val} | _] -> val
[] -> nil
end
end
defp perform_action(which, :foldl, fun), do: :dets.foldl(fun, [], which)
defp perform_action(which, :truncate, _), do: :dets.delete_all_objects(which)
defp perform_action(which, :delete, key), do: :dets.delete(which, key)
defp perform_action(which, :put, kv), do: :dets.insert(which, kv)
defp perform_action(which, :match_delete, key_pattern),
do: :dets.match_delete(which, {key_pattern, :_})
defp perform_action(which, :match, key_pattern),
do: :dets.match(which, {key_pattern, :_})
defp store(which, clump_id, :open) do
{:ok, ^which} = :dets.open_file(which, file: proper_db_path(which, clump_id))
end
defp store(which, _clump_id, :close), do: :dets.close(which)
@doc """
Retrieve the current hash of the `:content` or `:identity` store.
No information should be gleaned from any particular hash beyond whether
the contents have changed since a previous check.
"""
def current_hash(which, clump_id \\ "default")
def current_hash(which, clump_id) do
case action(:status, clump_id, :get, {clump_id, which}) do
nil -> recompute_hash(clump_id, which)
hash -> hash
end
end
defp recompute_hash(clump_id, table)
defp recompute_hash(_, :status), do: "nahnah"
# This one should probably have this available at some point
defp recompute_hash(_, :metadata), do: "nahnah"
defp recompute_hash(clump_id, which) do
stuff =
case which do
:content -> Baobab.all_entries(clump_id)
:identity -> Baobab.Identity.list()
end
hash =
stuff
|> :erlang.term_to_binary()
|> Blake2.hash2b(7)
|> BaseX.Base62.encode()
# Even though identities are the same in both
# I might be convinced otherwise later
action(:status, clump_id, :put, {{clump_id, which}, hash})
hash
end
@doc """
Deal with the peristed bamboo content
"""
def content(subject, action, entry_id, clump_id, addlval \\ nil)
def content(subject, action, {author, log_id, seq}, clump_id, addlval) do
store(:content, clump_id, :open)
key = {author |> Identity.as_base62(), log_id, seq}
curr = perform_action(:content, :get, key)
actval =
case {action, curr} do
{:delete, nil} ->
:ok
{:delete, _} ->
perform_action(:content, :delete, key)
{:contents, nil} ->
case subject do
:both -> {:error, :error}
_ -> :error
end
{:contents, map} ->
case subject do
:both -> {Map.get(map, :entry, :error), Map.get(map, :payload, :error)}
key -> Map.get(map, key, :error)
end
{:hash, %{^subject => c}} ->
YAMFhash.create(c, 0)
{:write, prev} ->
case subject do
:both ->
{entry, payload} = addlval
perform_action(:content, :put, {key, %{:entry => entry, :payload => payload}})
map_key ->
map = if is_nil(prev), do: %{}, else: prev
perform_action(:content, :put, {key, Map.merge(map, %{map_key => addlval})})
end
{:exists, nil} ->
false
{:exists, _} ->
true
{_, _} ->
:error
end
store(:content, clump_id, :close)
actval
end
@doc false
# Handle the simplest case first
def retrieve(author, seq, {:binary, log_id, false, clump_id}) do
entry_id = {author, log_id, seq}
case content(:both, :contents, entry_id, clump_id) do
{:error, _} -> :error
{_, :error} -> :error
{entry, payload} -> entry <> payload
end
end
# This handles the other three cases:
# :entry validated or unvalidated
# :binary validated
def retrieve(author, seq, {fmt, log_id, validate, clump_id}) do
entry_id = {author, log_id, seq}
binary = content(:entry, :contents, entry_id, clump_id)
res = Entry.from_binaries(binary, validate, clump_id) |> hd
case {res, fmt} do
{{:error, :missing}, _} ->
:error
{:error, _} ->
content(:entry, :delete, entry_id, clump_id)
:error
{entry, :entry} ->
entry
{_, :binary} ->
binary
end
end
defp proper_db_path(:identity, clump_id) when byte_size(clump_id) > 0,
do: proper_db_path(:identity, "")
defp proper_db_path(which, clump_id) when is_binary(clump_id) and is_atom(which) do
file = Atom.to_string(which) <> ".dets"
dir = Application.fetch_env!(:baobab, :spool_dir) |> Path.expand()
Path.join([dir, clump_id, file]) |> to_charlist
end
defp proper_db_path(_, _), do: raise("Improper clump_id")
end