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 @moduledoc""" Internal state struct of directory shard. """ defstruct [:owner, :public, :name, :manifest, :id, :netgroup, :items, :revitems] end @doc""" Start a process that connects to a given channel. Don't call directly, use for instance: Shard.Manager.find_or_start %SApp.Directory.Manifest{owner: my_pk, public: false, name: "collection"} """ 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 with `stored = true` 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