lib/api.ex

defmodule MyspaceIPFS.Api do
  @moduledoc """
  IPFS (the InterPlanetary File Syste
  new hypermedia distribution protocol, addressed by
  content and identities. IPFS enables the creation of
  completely distributed applications. It aims to make the web
  faster, safer, and more open.


  IPFS is a distributed file system that seeks to connect
  all computing devices with the same system of files. In some
  ways, this is similar to the original aims of the Web, but IPFS
  is actually more similar to a single bittorrent swarm exchanging
  git objects.

  Forked from https://github.com/tensor-programming/Elixir-Ipfs-Api

  Based on https://github.com/tableturn/ipfs/blob/master/lib/ipfs.ex
  """
  use Tesla, docs: false
  alias Tesla.Multipart

  # Config
  @baseurl Application.get_env(:myspace_ipfs, :baseurl)
  @debug Application.get_env(:myspace_ipfs, :debug)

  # Types
  @typep path :: MyspaceIPFS.path()
  @typep fspath :: MyspaceIPFS.fspath()
  @typep opts :: MyspaceIPFS.opts()
  @typep result :: MyspaceIPFS.result()

  # Middleware
  plug(Tesla.Middleware.BaseUrl, @baseurl)
  @debug && plug(Tesla.Middleware.Logger)

  @doc """
  High level function allowing to perform POST requests to the node.
  A `path` has to be provided, along with an optional list of `opts` that are
  dependent on the endpoint that will get hit.
  NB! This is not a GET request, but a POST request. IPFS uses POST requests.
  """
  @spec post_query(path, opts) :: result
  def post_query(path, opts \\ []) do
    handle_response(post(@baseurl <> path, "", opts))
  end

  @doc """
  High level function allowing to send file contents to the node.
  A `path` has to be specified along with the `fspath` to be sent. Also, a list
  of `opts` can be optionally sent.
  """
  @spec post_file(path, fspath, opts) :: result
  def post_file(path, fspath, opts \\ []) do
    handle_response(post(path, multipart(fspath), opts))
  end

  defp handle_response(response) do
    # Handles the response from the node. It returns the body of the response
    # if the status code is 200, otherwise it returns an error tuple.
    # ## Status codes that are handled
    # https://docs.ipfs.tech/reference/kubo/rpc/#http-status-codes
    #   - 200 - The request was processed or is being processed (streaming)
    #   - 500 - RPC Endpoint returned an error
    #   - 400 - Malformed RPC, argument type error, etc.
    #   - 403 - RPC call forbidden
    #   - 404 - RPC endpoint does not exist
    #   - 405 - RPC endpoint exists but method is not allowed
    case response do
      {:ok, %Tesla.Env{status: 200, body: body}} -> body
      {:ok, %Tesla.Env{status: 500}} -> {:eserver, response}
      {:ok, %Tesla.Env{status: 400}} -> {:eclient, response}
      {:ok, %Tesla.Env{status: 403}} -> {:eaccess, response}
      {:ok, %Tesla.Env{status: 404}} -> {:emissing, response}
      {:ok, %Tesla.Env{status: 405}} -> {:enoallow, response}
      {:error, _} -> {:error, response}
    end
  end

  # Thanks to some forum :-)
  defp ls_r(path \\ ".") do
    cond do
      File.regular?(path) ->
        [path]

      File.dir?(path) ->
        File.ls!(path)
        |> Enum.map(&Path.join(path, &1))
        |> Enum.map(&ls_r/1)
        |> Enum.concat()

      true ->
        []
    end
  end

  # This function is written explicitly to remove the base directory from the
  # file path. This is done so that the file path is relative to the base
  # directory. This is to avoid leaking irrelevant paths to the server.
  defp multipart_add_file(mp, fspath, basedir) do
    with relative_filename = String.replace(fspath, basedir <> "/", "") do
      Multipart.add_file(mp, fspath,
        name: "file",
        filename: "#{relative_filename}",
        detect_content_type: true
      )
    end
  end

  defp multipart_add_files(multipart, fspath) do
    with basedir = Path.dirname(fspath) do
      ls_r(fspath)
      |> Enum.reduce(multipart, fn fspath, multipart ->
        multipart_add_file(multipart, fspath, basedir)
      end)
    end
  end

  defp multipart(fspath) do
    Multipart.new()
    |> multipart_add_files(fspath)
  end
end