defmodule SApp.Directory do
@moduledoc"""
Shard application for a directory of other shards.
Items can be stored (they become dependencies) or just links for possible reference.
TODO: use MST for item list instead of plain list
"""
use GenServer
require Logger
defmodule Manifest do
@moduledoc"""
Manifest for a directory. This directory is owned by a user,
has a name, and can be either public or private.
"""
defstruct [:owner, :public, :name]
defimpl Shard.Manifest do
def module(_m), do: SApp.Directory
def is_valid?(m) do
is_boolean(m.public)
and is_binary(m.name)
and byte_size(m.owner) == 32
end
end
end
defmodule State do
defstruct [:owner, :public, :name, :manifest, :id, :netgroup, :items, :revitems]
end
def start_link(manifest) do
GenServer.start_link(__MODULE__, manifest)
end
def init(manifest) do
%Manifest{owner: owner, public: public, name: name} = manifest
id = SData.term_hash manifest
Shard.Manager.dispatch_to(id, nil, self())
items = case Shard.Manager.load_state(id) do
nil ->
SData.SignRev.new %{}
st -> st
end
netgroup = case public do
true -> %SNet.PubShardGroup{id: id}
false -> %SNet.PrivGroup{pk_list: [owner]}
end
SNet.Group.init_lookup(netgroup, self())
revitems = for {n, m} <- SData.SignRev.get(items), into: %{}, do: {m, n}
{:ok, %State{
owner: owner, public: public, name: name,
manifest: manifest, id: id, netgroup: netgroup,
items: items, revitems: revitems}}
end
def handle_call(:manifest, _from, state) do
{:reply, state.manifest, state}
end
def handle_call(:delete_shard, _from, state) do
{:stop, :normal, :ok, state}
end
def handle_call(:get_items, _from, state) do
{:reply, SData.SignRev.get(state.items), state}
end
def handle_call({:add_item, name, manifest, stored}, _from, state) do
if Shard.Keys.have_sk?(state.owner) do
dict = SData.SignRev.get(state.items)
if dict[name] != nil and elem(dict[name], 0) != manifest do
{:reply, :exists_already, state}
else
dict = Map.put(dict, name, {manifest, stored})
{:ok, st2} = SData.SignRev.set(state.items, dict, state.owner)
Shard.Manager.save_state(state.id, st2)
state = %{state |
items: st2,
revitems: Map.put(state.revitems, manifest, name)
}
bcast_state(state)
send_deps(state)
{:reply, :ok, state}
end
else
{:reply, :impossible, state}
end
end
def handle_call({:rm_item, item}, _from, state) do
if Shard.Keys.have_sk?(state.owner) do
dict = SData.SignRev.get(state.items)
case find(state, dict, item) do
{name, manifest} ->
state = put_in(state.revitems, Map.delete(state.revitems, manifest))
dict = Map.delete(dict, name)
{:ok, st2} = SData.SignRev.set(state.items, dict, state.owner)
Shard.Manager.save_state(state.id, st2)
state = put_in(state.items, st2)
bcast_state(state)
send_deps(state)
{:reply, :ok, state}
nil ->
{:reply, :not_found, state}
end
else
{:reply, :impossible, state}
end
end
def handle_call({:set_stored, item, stored}, _from, state) do
if Shard.Keys.have_sk?(state.owner) do
dict = SData.SignRev.get(state.items)
case find(state, dict, item) do
{name, manifest} ->
dict = Map.put(dict, name, {manifest, stored})
{:ok, st2} = SData.SignRev.set(state.items, dict, state.owner)
Shard.Manager.save_state(state.id, st2)
state = put_in(state.items, st2)
bcast_state(state)
send_deps(state)
{:reply, :ok, state}
nil ->
{:reply, :not_found, state}
end
else
{:reply, :impossible, state}
end
end
def handle_call({:read, name}, _from, state) do
dict = SData.SignRev.get(state.items)
{:reply, dict[name], state}
end
def handle_call({:find, manifest}, _from, state) do
{:reply, state.revitems[manifest], state}
end
def handle_cast(:send_deps, state) do
send_deps(state)
{:noreply, state}
end
def handle_cast({:peer_connected, peer_pid}, state) do
SNet.Manager.send_pid(peer_pid, {:interested, [state.id]})
{:noreply, state}
end
def handle_cast({:interested, peer_pid, auth}, state) do
if SNet.Group.in_group?(state.netgroup, peer_pid, auth) do
SNet.Manager.send_pid(peer_pid, {state.id, nil, {:update, SData.SignRev.signed(state.items), true}})
end
{:noreply, state}
end
def handle_cast({:msg, conn_pid, auth, _shard_id, nil, msg}, state) do
if not SNet.Group.in_group?(state.netgroup, conn_pid, auth) do
{:noreply, state}
else
state = case msg do
{:update, signed, ask_reply} when signed != nil ->
state = case SData.SignRev.merge(state.items, signed, state.pk) do
{true, newitems} ->
Shard.Manager.save_state(state.id, newitems)
state = put_in(state.items, newitems)
bcast_state(state, [conn_pid])
state
{false, _} ->
state
end
if ask_reply do
SNet.Manager.send_pid(conn_pid, {state.id, nil, {:update, SData.SignRev.signed(state.items), false}})
end
state
_ -> state
end
{:noreply, state}
end
end
defp find(state, dict, item) do
cond do
dict[item] != nil ->
{manifest, _} = dict[item]
{item, manifest}
state.revitems[item] != nil ->
name = state.revitems[item]
{name, item}
true ->
nil
end
end
defp bcast_state(state, exclude \\ []) do
msg = {state.id, nil, {:update, SData.SignRev.signed(state.items), false}}
SNet.Group.broadcast(state.netgroup, msg, exclude_pid: exclude)
end
defp send_deps(state) do
dict = SData.SignRev.get(state.items)
deps = for {_, {m, stored}} <- dict, stored, do: m
GenServer.cast(Shard.Manager, {:dep_list, state.id, deps})
end
# ================
# PUBLIC INTERFACE
# ================
@doc"""
Return list of items stored in this directory.
Returns a dictionnary of %{name => {manifest, stored?}}.
"""
def get_items(pid) do
GenServer.call(pid, :get_items)
end
@doc"""
Return the manifest of item with a given name in directory, or nil if not found.
Equivalent to get_items(pid)[name] but better.
"""
def read(pid, name) do
GenServer.call(pid, {:read, name})
end
@doc"""
Find an item in the directory by its manifest. Returns name if found or nil if not found.
"""
def find(pid, manifest) do
GenServer.call(pid, {:find, manifest})
end
@doc"""
Add an item to this directory. An item is a name for a shard manifest.
An item added to a directory becomes a dependency of the directory, i.e.
if the directory is pinned then all items inside are pinned as well.
"""
def add_item(pid, name, manifest, stored \\ true) do
GenServer.call(pid, {:add_item, name, manifest, stored})
end
@doc"""
Remove a named item from this directory.
Argument can be either a manifest or the name of an item.
"""
def rm_item(pid, item) do
GenServer.call(pid, {:rm_item, item})
end
@doc"""
Set an item as stored or not stored.
Argument can be either a manifest or the name of an item.
"""
def set_stored(pid, item, stored) do
GenServer.call(pid, {:set_stored, item, stored})
end
end