lib/ex_ipfs.ex

defmodule ExIpfs do
  @moduledoc """
  Core commands and types for ExIpfs.
  In order for this module to work you need to have an IPFS daemon running. See README.md for details.

        ## Examples

        iex> alias ExIpfs, as: Ipfs
        iex> Ipfs.cat("QmZ4tDuvesekSs4qM5ZBKpXiZGun7S2CYtEZRB3DYXkjGx")
        <<0, 19, 148, 0, ... >>
  """

  require Logger

  import ExIpfs.Api
  import ExIpfs.Utils

  @typedoc """
  A struct that represents the result of adding a file to IPFS.
  """
  @type add_result :: %ExIpfs.AddResult{
          bytes: non_neg_integer(),
          hash: binary(),
          name: binary(),
          size: non_neg_integer()
        }

  @typedoc """
  A struct for the ID returned by the id command.
  """
  @type id :: %ExIpfs.Id{
          addresses: list,
          agent_version: String.t(),
          id: String.t(),
          protocol_version: String.t(),
          public_key: String.t(),
          protocols: list
        }

  @typedoc """
  This struct is very simple. Some results are listed as "Value": size. This is a
  convenience struct to make it easier match on the result.
  """
  @type key_value :: %ExIpfs.KeyValue{key: binary(), value: binary()}

  @typedoc """
  A struct for the links of a DAG in IPLD. When IPLD sees such a Key Value in the JSON result it will lookup the data.
  """
  @type link :: %ExIpfs.Link{/: binary()}

  @typedoc """
  ExIpfs.MultibaseCodec is a struct representing a hash. Seems much like a codec structure to me, but hey. Things may differ.
  """
  @type multi_codec :: %ExIpfs.Multicodec{
          name: binary(),
          code: non_neg_integer()
        }

  @typedoc """
  A Multihash.
  """
  @type multi_hash :: %ExIpfs.Multihash{
          name: binary(),
          code: non_neg_integer()
        }

  @typedoc """
  An object in IPFS with a hash and links to other objects.
  """
  @type object :: %ExIpfs.Object{
          hash: binary(),
          links: list(object())
        }

  # @typedoc """
  # A struct for a hash in the hash links list in Objects.
  # """
  # @type object :: %ExIpfs.Object{
  #         hash: binary(),
  #         name: binary(),
  #         size: non_neg_integer(),
  #         target: binary(),
  #         type: non_neg_integer()
  #       }

  # @typedoc """
  # A struct for the links of hash in Objects.
  # """
  # @type object_links :: %ExIpfs.ObjectLinks{
  #         hash: binary(),
  #         links: list(object())
  #       }

  @typedoc """
  B58 encoded peer ID.
  """
  @type peer_id() :: <<_::48, _::_*8>>

  @doc """
  Resolve the value of names to IPFS.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-resolve
  ```
  [
    recursive: true,
    nocache: true,
    dht-record-count: 10,
    dht-timeout: 10
  ]
  ```
  """
  @spec resolve(Path.t(), list) :: {:ok, Path.t()} | ExIpfs.Api.error_response()
  def resolve(path, opts \\ []),
    do:
      post_query("/resolve?arg=" <> path, query: opts)
      |> okify()

  @doc """
  Add a file to IPFS.

  ## Parameters
  * `fspath` - The file system path to the file or directory to be sent to the node.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-add


  """
  @spec add_fspath(Path.t(), list) :: add_result | ExIpfs.Api.error_response()
  def add_fspath(fspath, opts \\ []),
    do:
      multipart(fspath)
      |> post_multipart("/add", query: opts)
      |> ExIpfs.AddResult.new()
      |> okify()

  @doc """
  Add a file to IPFS.

  ## Parameters
  * `data` - The data to be sent to the IPFS node.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-add


  """
  # FIXME return a struct
  @spec add(any, list) :: {:ok, add_result()} | ExIpfs.Api.error_response()
  def add(data, opts \\ []),
    do:
      multipart_content(data)
      |> post_multipart("/add", query: opts)
      |> ExIpfs.AddResult.new()
      |> okify()

  @doc """
  Get the contents of a file from ipfs.
  Easy way to get the contents of a text file for instance.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-cat
  ```
  [
    offset: <int64>,
    length: <int64>,
    progress: <bool>
  ]
  ```
  """
  # FIXME: return a struct
  @spec cat(Path.t(), list) :: {:ok, any} | ExIpfs.Api.error_response()
  def cat(path, opts \\ []),
    do: post_query("/cat?arg=" <> path, query: opts)

  @doc """
  Get a file or directory from IPFS.

  *NB! Unsafe (relative symlinks) will raise an error.* This is a limitation of the underlying library.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-get
  ```
  [
    output: <string>, # Output to file or directory name. Optional, default: <cid-ipfs-or-ipns-path>
    archive: <bool>, # Output as a tarball. Optional, default: false
    timeout: <int64>, # Timeout in seconds. Optional, default: 100
  ]
  ```
  Compression is not implemented.

  If you feel that you need more timeouts, you can use the `:timeout` option in the `opts` list.
  But the default should be enough for most cases. More likely your content isn't available....
  """
  @spec get(Path.t(), list) :: {:ok, Path.t()} | ExIpfs.Api.error_response()
  defdelegate get(path, opts \\ []), to: ExIpfs.Get
  # # # @doc """
  # # # Get a file or directory from IPFS.

  # # # *NB! Unsafe (relative symlinks) will raise an error.* This is a limitation of the underlying library.

  # # # ## Options
  # # # https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-get
  # # # ```
  # # # [
  # # #   output: <string>, # Output to file or directory name. Optional, default: <cid-ipfs-or-ipns-path>
  # # #   archive: <bool>, # Output as a tarball. Optional, default: false
  # # #   timeout: <int64>, # Timeout in seconds. Optional, default: 100
  # # # ]
  # # # ```
  # # # Compression is not implemented.

  # # # If you feel that you need more timeouts, you can use the `:timeout` option in the `opts` list.
  # # # But the default should be enough for most cases. More likely your content isn't available....
  # # # """
  # # # @spec get(Path.t(), list) :: {:ok, Path.t()} | ExIpfs.Api.error_response()
  # # # defdelegate get(path, opts \\ []), to: ExIpfs.Get

  @doc """
  Show the id of the IPFS node.

  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-id
  Returns a map with the following keys:
    - ID: the id of the node.
    - PublicKey: the public key of the node.
    - Addresses: the addresses of the node.
    - AgentVersion: the version of the node.
    - ProtocolVersion: the protocol version of the node.
    - Protocols: the protocols of the node.
  """
  @spec id :: {:ok, id()} | ExIpfs.Api.error_response()
  def id,
    do:
      post_query("/id")
      |> ExIpfs.Id.new()
      |> okify()

  @doc """
  List directory contents for Unix filesystem objects in IPFS.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-ls
  ```
  [
    headers: <bool>, # Print table headers (Hash, Size, Name). Optional, default: false
    resolve-type: <bool>, # Resolve linked objects to find out their types. Optional, default: false
    timeout: <int64>, # Timeout in seconds. Optional, default: 100
  ]
  ```

  Streaming is not supported yet, but might be in there future. Post a feature request if you need it.
  """
  @spec ls(Path.t(), list) :: {:ok, list(object())} | ExIpfs.Api.error_response()
  def ls(path, opts \\ []) do
    with %{"Objects" => objects} <- post_query("/ls?arg=" <> path, query: opts) do
      {:ok, Enum.map(objects, &ExIpfs.Object.new(&1))}
    end
  end

  # |> ExIpfs.Objects.new()
  # |> okify()

  @doc """
  Ping a peer in the IPFS network.

  """
  # coveralls-ignore-start
  # We don't have anything to ping. So we can't test this.
  @spec ping(peer_id, pid, atom | integer, list) :: :ignore | {:error, any} | {:ok, pid}
  def ping(peer_id, pid \\ self(), timeout \\ :infinity, opts \\ []) do
    request = ExIpfs.PingRequest.new(peer_id, pid, timeout, opts)
    ExIpfs.Ping.new(request)
  end

  # coveralls-ignore-stop
end