aboutsummaryrefslogblamecommitdiff
path: root/shard/lib/app/directory.ex
blob: 257e8b994c2e13e9fc572408a99fe3bd1d9b9a9b (plain) (tree)
1
2
3
4
5
6


                                                    
                                                                                      
 
                                                   















                                                                




                                    


       
                    



                                             
                                                                                    

     




                                                                                                           








                                                                  
                                                









                                                
                                                                            
 
                 

                                                     
                                        





                                             



                                                 

                                                   

     
                                                                       
                                          

                                                                 

                                        

                                                                      
                                               
                         

                                                           
         
                          
                        






                                  
                                                    
                                          
































                                                                              





                                  
                                                 
                                         



                                                     
                                             

     
                                       
                    


                     




                                                              

                                                              
                                                                                                          









                                                                        



                                                                            





                                            
                                                                                                                 







                       












                                    
                                           
                                                                              


                                                                   

                                         
                                                      


                                                              




                    
                                                
 
                                                            
     

                                   


         
                                                                                    
  
                                                  





                                      
                                                                                             





                                          
                                                                        

                                                                                          







                                                            
     

                                         


         

                                                           
     

                                                    

     
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