aboutsummaryrefslogblamecommitdiff
path: root/shard/lib/app/directory.ex
blob: a15de21ed2faf286418eb6e7c9a9432a8e22d86e (plain) (tree)























                                                                



                                                                                    



















                                                                  

                                                                            
                 

                                                     
                                        



















                                                                              



                                                           













                                                    
                                                                              












                                                                              








                                                     

















































                                                                                                                 
                                               





                                   















                                                                                          













                                                                         







                                                                   
   
defmodule SApp.Directory do
  @moduledoc"""
  Shard application for a directory of other shards.

  TODO: use MST for file 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
    end
  end

  defmodule State do
    defstruct [:owner, :public, :name, :manifest, :id, :netgroup, :files, :revfiles]
  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())
    files = 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())

    revfiles = for {n, m} <- SData.SignRev.get(files), into: %{}, do: {m, n}

    {:ok, %State{
      owner: owner, public: public, name: name,
      manifest: manifest, id: id, netgroup: netgroup,
      files: files, revfiles: revfiles}}
  end

  def handle_call(:manifest, _from, state) do
    {:reply, state.manifest, state}
  end

  def handle_call(:get_files, _from, state) do
    {:reply, SData.SignRev.get(state.files), state}
  end

  def handle_call({:add_file, name, manifest}, _from, state) do
    if Shard.Keys.have_sk?(state.owner) do
      dict = SData.SignRev.get(state.files)
      if dict[name] != nil and dict[name] != manifest do
        {:reply, :exists_already, state}
      else
        dict = Map.put(dict, name, manifest)
        GenServer.cast(Shard.Manager, {:dep_list, state.id, Map.values(dict)})
        {:ok, st2} = SData.SignRev.set(state.files, dict, state.owner)
        Shard.Manager.save_state(state.id, st2)
        state = %{state |
          files: st2,
          revfiles: Map.put(state.revfiles, manifest, name)
        }
        bcast_state(state)
        {:reply, :ok, state}
      end
    else
      {:reply, :impossible, state}
    end
  end

  def handle_call({:rm_file, name}, _from, state) do
    if Shard.Keys.have_sk?(state.owner) do
      dict = SData.SignRev.get(state.files)
      if dict[name] == nil do
        {:reply, :not_found, state}
      else
        state = put_in(state.revfiles, Map.delete(state.revfiles, dict[name]))
        dict = Map.delete(dict, name)
        GenServer.cast(Shard.Manager, {:dep_list, state.id, Map.values(dict)})
        {:ok, st2} = SData.SignRev.set(state.files, dict, state.owner)
        Shard.Manager.save_state(state.id, st2)
        state = put_in(state.files, st2)
        bcast_state(state)
        {:reply, :ok, state}
      end
    else
      {:reply, :impossible, state}
    end
  end

  def handle_call({:read, name}, _from, state) do
    dict = SData.SignRev.get(state.files)
    {:reply, dict[name], state}
  end

  def handle_call({:find, manifest}, _from, state) do
    {:reply, state.revfiles[manifest], state}
  end

  def handle_cast(:send_deps, state) do
    dict = SData.SignRev.get(state.files)
    GenServer.cast(Shard.Manager, {:dep_list, state.id, Map.values(dict)})
    {: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.files), 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.files, signed, state.pk) do
            {true, newfiles} ->
              Shard.Manager.save_state(state.id, newfiles)
              state = put_in(state.files, newfiles)
              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.files), false}})
          end
          state
      _ -> state
      end
      {:noreply, state}
    end
  end

  defp bcast_state(state, exclude \\ []) do
    msg = {state.id, nil, {:update, SData.SignRev.signed(state.files), false}}
    SNet.Group.broadcast(state.netgroup, msg, exclude_pid: exclude)
  end

  # ================
  # PUBLIC INTERFACE
  # ================

  @doc"""
  Return list of files stored in this directory.

  Returns a dictionnary of %{name => manifest}.
  """
  def get_files(pid) do
    GenServer.call(pid, :get_files)
  end

  @doc"""
  Return the manifest of file with a given name in directory, or nil if not found.
  
  Equivalent to get_files(pid)[name] but better.
  """
  def read(pid, name) do
    GenServer.call(pid, {:read, name})
  end

  @doc"""
  Find a file 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 a file to this directory. A file is a name for a shard manifest.
  A file added to a directory becomes a dependency of the directory, i.e.
  if the directory is pinned then all files inside are pinned as well.
  """
  def add_file(pid, name, manifest) do
    GenServer.call(pid, {:add_file, name, manifest})
  end

  @doc"""
  Remove a named file from this directory.
  """
  def rm_file(pid, name) do
    GenServer.call(pid, {:rm_file, name})
  end

  @doc"""
  Returns the friends directory of a user
  """
  def friends_dir(pk) do
    manifest = %Manifest{name: "friends", owner: pk, public: false}
    Shard.Manager.find_or_start manifest
  end
end