Current section
Files
Jump to
Current section
Files
lib/ipfs.ex
defmodule IPFS do
@moduledoc """
Provides abstration allowing to access IPFS nodes with low effort.
"""
alias HTTPoison.{AsyncResponse, Response, Error}
@typedoc "Represents an endpoint path to hit."
@type path :: String.t()
@typedoc "Identifies content on the IPFS network using a multihash string."
@type cid :: String.t()
@typedoc "Represents an absolute filename existing on disk."
@type filename :: String.t()
@typedoc "Models the result of most of the functions accessible in this module."
@type result :: {:ok, any} | {:error, any}
@typedoc "Represents the endpoint to hit. Required as the first argument of most functions."
@type t :: %__MODULE__{scheme: String.t(), host: String.t(), port: pos_integer, base: path}
defstruct scheme: "http", host: "localhost", port: 5001, base: "api/v0"
@doc "Retrieves version information about the running IPFS node."
@spec version(t) :: result
def version(conn) do
conn
|> get("version")
|> remap_fields(version: "Version", commit: "Commit", system: "System", golang: "Golang")
|> okify
end
@typep poison_result :: {:ok, Response.t() | AsyncResponse.t()} | {:error, Error.t()}
# Key management.
@doc """
Generates a new keypair given its `name` and optional `type` and `size`.
Note that `type` could be either one of `:rsa` or `:ed25519` only.
Returns a `result`.
"""
@spec key_gen(t, String.t(), :rsa | :ed25519, pos_integer) :: result
def key_gen(conn, name, type \\ :rsa, size \\ 2048) do
conn
|> get("key/gen", arg: name, type: type, size: size)
|> remap_fields(id: "Id")
|> okify
end
@doc "Lists all keys stored in the local node."
@spec key_list(t) :: result
def key_list(conn) do
with {:ok, %{"Keys" => keys}} <- get(conn, "key/list") do
{:ok, keys}
|> remap_array(id: "Id", name: "Name")
|> okify
end
end
@doc """
Renames a keypair identified by its `name`. If `force` is set to `true`, existing
named with a matching `new_name` will be overwritten.
"""
@spec key_rename(t, String.t(), String.t(), boolean) :: result
def key_rename(conn, name, new_name, force \\ false) do
conn
|> get("key/rename", arg: name, arg: new_name, force: force)
|> successify_with
end
@doc "Removes a keypair identified by its `name`."
def key_rm(conn, name) do
conn
|> get("key/rm", arg: name)
|> successify_with
end
# Content management.
@doc """
Adds content identified by its `filename` on disk to the IPFS network.
A range of `params` can be specified, please refer to the official
[IPFS documentation](https://ipfs.io/docs/api/#apiv0add) for more information.
"""
@spec add(t, filename, keyword) :: result
def add(conn, filename, params \\ []) do
conn
|> post_file("add", filename, params)
|> remap_fields(name: "Name", hash: "Hash", size: "Size")
|> okify
end
# Pinning.
@doc "List all pins registered on this node."
@spec pin_ls(t) :: result
def pin_ls(conn) do
with {:ok, %{"Keys" => keys}} <- get(conn, "pin/ls") do
{:ok, keys}
end
end
@doc "Instructs the node to pin a given `cid`."
@spec pin_add(t, cid) :: result
def pin_add(conn, cid) do
conn
|> get("pin/add", arg: cid)
|> successify_with
end
@doc "Instructs the node to let go of a given `cid`."
@spec pin_rm(t, cid) :: result
def pin_rm(conn, cid) do
conn
|> get("pin/rm", arg: cid)
|> successify_with
end
# Generic request helpers.
@doc """
High level function allowing to perform GET requests to the node.
A `path` has to be provided, along with an optional list of `params` that are
dependent on the endpoint that will get hit.
"""
@spec get(t, path, keyword) :: result
def get(conn, path, params \\ []) do
request(conn, path, &HTTPoison.get(&1, [], params: params))
end
@doc """
High level function allowing to send file contents to the node.
A `path` has to be specified along with the `filename` to be sent. Also, a list
of `params` can be optionally sent.
"""
@spec post_file(t, path, filename, keyword) :: result
def post_file(conn, path, filename, params \\ []) do
request(conn, path, &HTTPoison.post(&1, multipart(filename), params: params))
end
# Private stuff.
@spec request(t, path, (String.t() -> poison_result)) :: result
defp request(conn, path, requester) do
conn
|> (&"#{&1.scheme}://#{&1.host}:#{&1.port}/#{&1.base}/#{path}").()
|> requester.()
|> to_result
end
@spec to_result(poison_result) :: result
defp to_result({:ok, %Response{status_code: 200, body: b}}) do
case Poison.decode(b) do
{:ok, _} = res -> res
{:error, :invalid, 0} -> {:ok, %{}}
otherwise -> otherwise
end
end
defp to_result({:ok, %Response{status_code: 200}}) do
%{}
end
defp to_result({:ok, %Response{status_code: c, body: b}}) do
{:error, "Error status code: #{c}, #{b}"}
end
defp to_result({:error, %Error{reason: err}}) do
{:error, err}
end
@spec multipart(filename) ::
{:multipart,
[{:file, filename, {String.t(), [name: String.t(), filename: filename]}, []}]}
defp multipart(filename) do
{:multipart,
[{:file, filename, {"form-data", [name: Path.basename(filename), filename: filename]}, []}]}
end
@spec remap_fields(result, [{atom, String.t()}]) :: %{}
defp remap_fields(res, mapping) do
with {:ok, data} <- res do
mapping
|> Enum.into(%{})
|> Enum.reduce(%{}, fn {to, from}, acc ->
case Map.fetch(data, from) do
{:ok, value} -> Map.put(acc, to, value)
_ -> acc
end
end)
end
end
@spec remap_array(result, [{atom, String.t()}]) :: []
defp remap_array(res, mapping) do
with {:ok, data} when is_list(data) <- res do
data
|> Enum.reduce([], fn item, acc ->
[remap_fields({:ok, item}, mapping)] ++ acc
end)
end
end
@spec okify(any) :: {:ok, any}
defp okify(res), do: {:ok, res}
@spec successify_with(result) :: result
defp successify_with({:ok, _}), do: okify(:success)
defp successify_with(err), do: err
end