lib/ipfs.ex

defmodule MyspaceIPFS do
  @moduledoc """
  MyspaceIPFS.Api is where the main commands of the IPFS API reside.
  Alias this library and you can run the commands via Api.<cmd_name>.

        ## Examples

        iex> alias MyspaceIPFS.API, as: Api
        iex> Api.get("Multihash_key")
        <<0, 19, 148, 0, ... >>
  """

  import MyspaceIPFS.Api
  import MyspaceIPFS.Utils

  @experimental Application.get_env(:myspace_ipfs, :experimental)

  # Types
  @typedoc """
  The path to the endpoint to be hit. For example, `/add` or `/cat`.
  It's called path because sometimes the MultiHash is not enough to
  identify the resource, and a path is needed, eg. /ipns/myspace.bahner.com
  """
  @type path :: String.t()
  @typedoc """
  The file system path to the file to be sent to the node.
  """
  @type fspath :: String.t()
  @typedoc """
  The name of the file or data to be sent to the node.
  """
  @type name :: String.t()
  @typedoc """
  The options to be sent to the node. These are dependent on the endpoint
  """
  @type opts :: list

  @typedoc """
  The structure of a normal error response from the node.
  """
  @type error ::
          {:error, Tesla.Env.t()}
          | {:eserver, Tesla.Env.t()}
          | {:eclient, Tesla.Env.t()}
          | {:eaccess, Tesla.Env.t()}
          | {:emissing, Tesla.Env.t()}
          | {:enoallow, Tesla.Env.t()}
  @typedoc """
  The structure of a normal response from the node.
  """
  @type mapped :: {:ok, list} | {:error, Tesla.Env.t()}
  @typedoc """
  The structure of a JSON response from the node.
  """
  @type okresult :: {:ok, any} | {:error, Tesla.Env.t()}
  @typedoc """
  The structure of a JSON response from the node.
  """
  @type result :: any | {:error, Tesla.Env.t()}

  @doc """
  Start the IPFS daemon.

  You should run this before any other command, but it's probably easier to do outside of the library.

  ## Options
  https://docs.ipfs.tech/reference/kubo/cli/#ipfs-daemon

  ```
  [
    "--init", # <bool> Initialize IPFS with default settings, if not already initialized
    "--migrate", # <bool> If answer yes to migration prompt
    "--init-config <string>", # Path to the configuration file to use
    "--init-profile <string>", # Apply profile settings to config
    "--routing <string>", # Override the routing system
    "--mount", # <bool> Mount IPFS to the filesystem (experimental)
    "--writable", # <bool> Enable writing objects (with POST, PUT, DELETE)
    "--mount-ipfs <string>", # Path to the mountpoint for IPFS (if using --mount)
    "--mount-ipns <string>", # Path to the mountpoint for IPNS (if using --mount)
    "--unrestricted-api", # <bool> Allow API access to unlisted hashes
    "--disable-transport-encryption", # <bool> Disable transport encryption (for debugging)
    "--enable-gc", # <bool> Enable automatic repo garbage collection
    "--enable-pubsub-experiment", # <bool> Enable experimental pubsub
    "--enable-namesys-pubsub", # <bool> Enable experimental namesys pubsub
    "--agent-version-suffix <string>", # Suffix to append to the AgentVersion string for id()
  ]
  ```
  """
  def daemon(start? \\ true, flag \\ [], opts \\ []) do
    {:ok, pid} = Task.start(fn -> System.cmd("ipfs", ["daemon"] ++ opts) end)

    if start? == false do
      pid |> shutdown(flag)
    else
      pid
    end
  end

  defp shutdown(pid, term) do
    Process.exit(pid, term)
  end

  @doc """
  Shutdown the IPFS daemon.
  """
  @spec shutdown :: result
  def shutdown, do: post_query("/shutdown")

  @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, opts) :: result
  def resolve(path, opts \\ []),
    do:
      post_query("/resolve?arg=" <> path, opts)
      |> map_response_data()
      |> okify()

  @doc """
  Add a file to IPFS.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-add
  """
  @spec add(fspath, opts) :: result
  def add(fspath, opts \\ []),
    do:
      post_file("/add", fspath, opts)
      |> map_response_data()
      |> okify()

  @doc """
  Get a file or directory from IPFS.
  As it stands ipfs sends a text blob back, so we need to implement a way to
  get the file extracted and saved to disk.
  The default name is the CID or the basename of the IPNS or IPFS path.

  *NB! Unsafe (relative symlinks) will raise an error.*

  Compression is not implemented yet.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-get
  ```
  [
    output: <string>, # Optional, default: Name of the object. CID or path basename.
    archive: <bool>, # Optional, default: false
    compress: <bool>, # NOT IMPLEMENTED
    compression_level: <int> # NOT IMPLEMENTED
  ]
  ```
  """
  @spec get(path, opts) :: result
  defdelegate get(path, opts \\ []), to: MyspaceIPFS.Get

  @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>
  ]
  ```
  """
  @spec cat(path, opts) :: result
  def cat(path, opts \\ []),
    do:
      post_query("/cat?arg=" <> path, opts)
      |> okify()

  @doc """
  List the files in an IPFS object.

  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-ls
  ```
  [
    headers: <bool>,
    resolve-type: <bool>,
    stream: <bool>,
    size: <bool>,
  ]
  ```

  ## Response
  ```
  {
    Objects: [
      {
        "Name": "string",
        "Hash": "string",
        "Size": 0,
        "Type": 0,
        "Links": [
          {
            "Name": "string",
            "Hash": "string",
            "Size": 0,
            "Type": 0
          }
        ]
      }
    ]
  }
  ```
  """
  @spec ls(path, opts) :: result
  def ls(path, opts \\ []),
    do:
      post_query("/ls?arg=" <> path, opts)
      |> Jason.decode!()
      |> okify()

  @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 :: result
  def id,
    do:
      post_query("/id")
      |> map_response_data()
      |> filter_empties()
      |> unlist()
      |> okify()

  @doc """
  Ping a peer.
  ## Parameters
  - peer: the peer to ping.
  ## Options
  https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-ping
  ```
  [
    n|count: <int>,
  ]
  ```
  """
  @spec ping(name, opts) :: result
  def ping(peer, opts \\ []),
    do:
      post_query("/ping?arg=" <> peer, opts)
      |> map_response_data()
      |> okify()

  if @experimental do
    @doc """
    Mount an IPFS read-only mountpoint.

    ## Options
    https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-mount
    ```
    [
      ipfs-path: <string>, # default: /ipfs
      ipns-path: <string>, # default: /ipns
    ]
    ```
    """
    @spec mount(opts) :: result
    def mount(opts \\ []) do
      post_query("/mount", opts)
      |> map_response_data()
      |> okify()
    end
  end
end