ruby-ipfs-http-client 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1728edc46e1056ee5b94ccc057f3c68ae4eabfab20518d54cb52ecd0fb1944bc
4
+ data.tar.gz: a7125b526b11263bb2e4b67100f64530b5d9633a2c503def3cb307488e4afebd
5
+ SHA512:
6
+ metadata.gz: 7131e40d3fd1b7634bb7af59b1b264cdac9136d906d83672db68471f18a314bcdbca32bc1e868c744b34fdc572262735d5dcbf52177a9d6a9cd4d627bb75a46b
7
+ data.tar.gz: 0ce36826dc172a6d5b72681aa448d69aba62b81a7c50b58848038e269d42067c68b781bd48541098728ad59e24f55d43359ac35a208b2c8776daa83cb9ca7a19
@@ -0,0 +1,5 @@
1
+ require_relative './ruby-ipfs-http-client/client'
2
+ require_relative './ruby-ipfs-http-client/file'
3
+
4
+ module Ipfs
5
+ end
@@ -0,0 +1,26 @@
1
+ require_relative '../multihash'
2
+
3
+ require_relative '../request/basic_request'
4
+ require_relative '../request/file_upload_request'
5
+
6
+ require_relative './generic/id'
7
+ require_relative './generic/version'
8
+ require_relative './files/cat'
9
+ require_relative './files/ls'
10
+ require_relative './files/add'
11
+
12
+ module Ipfs
13
+ module Command
14
+ def self.build_request(path, **arguments)
15
+ keys = arguments.keys
16
+
17
+ if keys.include?(:multihash)
18
+ BasicRequest.new(path, multihash: arguments[:multihash])
19
+ elsif keys.include?(:filepath)
20
+ FileUploadRequest.new(path, arguments[:filepath])
21
+ else
22
+ BasicRequest.new(path)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ module Ipfs
2
+ module Command
3
+ class Add
4
+ PATH = '/add'
5
+
6
+ def self.build_request(filepath)
7
+ Command.build_request(PATH, filepath: filepath)
8
+ end
9
+
10
+ def self.parse_response(response)
11
+ JSON.parse response
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ require_relative '../../dagstream'
2
+
3
+ module Ipfs
4
+ module Command
5
+ class Cat
6
+ PATH = '/cat'
7
+
8
+ def self.build_request(multihash)
9
+ Command.build_request(PATH, multihash: multihash)
10
+ end
11
+
12
+ def self.parse_response(response)
13
+ DagStream.new response
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Ipfs
2
+ module Command
3
+ class Ls
4
+ PATH = '/ls'
5
+
6
+ def self.build_request(multihash)
7
+ Command.build_request(PATH, multihash: Ipfs::Multihash.new(multihash))
8
+ end
9
+
10
+ def self.parse_response(response)
11
+ JSON.parse(response.body)['Objects'][0]['Links']
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Ipfs
2
+ module Command
3
+ class Id
4
+ PATH = '/id'
5
+
6
+ def self.build_request
7
+ Command.build_request(PATH)
8
+ end
9
+
10
+ def self.parse_response(response)
11
+ JSON.parse response
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Ipfs
2
+ module Command
3
+ class Version
4
+ PATH = '/version'
5
+
6
+ def self.build_request
7
+ Command.build_request(PATH)
8
+ end
9
+
10
+ def self.parse_response(response)
11
+ JSON.parse response
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ module Ipfs
2
+ class Base58
3
+ ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
4
+ BASE = ALPHABET.length
5
+
6
+ def self.decode(number)
7
+ valid?(number) \
8
+ ? to_base10(number)
9
+ : 0
10
+ end
11
+
12
+ def self.to_base10(base58_number)
13
+ base58_number
14
+ .reverse
15
+ .split(//)
16
+ .each_with_index
17
+ .reduce(0) do |base10_number, (base58_numeral, index)|
18
+ base10_number + ALPHABET.index(base58_numeral) * (BASE**index)
19
+ end
20
+ end
21
+
22
+ def self.valid?(number)
23
+ number.match?(/\A[#{ALPHABET}]+\z/)
24
+ end
25
+
26
+ def self.encode(base10_number)
27
+ base10_number.is_a?(Integer) \
28
+ ? to_base58(base10_number) \
29
+ : ''
30
+ end
31
+
32
+ def self.to_base58(base10_number)
33
+ base58_number = ''
34
+
35
+ begin
36
+ base58_number << ALPHABET[base10_number % BASE]
37
+ base10_number /= BASE
38
+ end while base10_number > 0
39
+
40
+ base58_number.reverse
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,141 @@
1
+ require 'http'
2
+ require 'uri'
3
+
4
+ require_relative './errors'
5
+ require_relative './api/command'
6
+ require_relative './api/generic/id'
7
+
8
+ require_relative './connection/default'
9
+ require_relative './connection/ipfs_config'
10
+ require_relative './connection/unreachable'
11
+
12
+ module Ipfs
13
+ # The client is not intended to be manipulated. It is a singleton class used
14
+ # to route commands and their corresponding requests.
15
+ #
16
+ # However, it contains certain, read-only, information that can be useful for
17
+ # debugging purposes.
18
+ class Client
19
+ # @api private
20
+ DEFAULT_BASE_PATH = '/api/v0'
21
+ # @api private
22
+ CONNECTION_METHODS = [
23
+ Connection::Default,
24
+ Connection::IpfsConfig,
25
+ Connection::Unreachable
26
+ ]
27
+
28
+ # @api private
29
+ class << self
30
+ def initialize
31
+ attempt_connection
32
+
33
+ retrieve_ids
34
+ retrieve_daemon_version
35
+
36
+ ObjectSpace.define_finalizer(self, proc { @@connection.close })
37
+ end
38
+
39
+
40
+ # @api private
41
+ def execute(command, *args)
42
+ command.parse_response call command.build_request *args
43
+ end
44
+
45
+ # Various debugging information concerning the Ipfs node itself
46
+ #
47
+ # @example
48
+ # Ipfs::Client.id
49
+ # #=> {
50
+ # peer_id: 'QmVnLbr9Jktjwx...',
51
+ # addresses: [
52
+ # "/ip4/127.0.0.1/tcp/4001/ipfs/QmVwxnW4Z8JVMDfo1jeFMNqQor5naiStUPooCdf2Yu23Gi",
53
+ # "/ip4/192.168.1.16/tcp/4001/ipfs/QmVwxnW4Z8JVMDfo1jeFMNqQor5naiStUPooCdf2Yu23Gi",
54
+ # "/ip6/::1/tcp/4001/ipfs/QmVwxnW4Z8JVMDfo1jeFMNqQor5naiStUPooCdf2Yu23Gi",
55
+ # "/ip6/2a01:e34:ef8d:2940:8f7:c616:...5naiStUPooCdf2Yu23Gi",
56
+ # "/ip6/2a01:e34:ef8d:2940:...5naiStUPooCdf2Yu23Gi",
57
+ # "/ip4/78.248.210.148/tcp/13684/ipfs/Qm...o1jeFMNqQor5naiStUPooCdf2Yu23Gi"
58
+ # ],
59
+ # public_key: "CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwgg...AgMBAAE=",
60
+ # agent_version: "go-ipfs/0.4.13/3b16b74"
61
+ # }
62
+ #
63
+ # @return [Hash{Symbol => String, Array<String>}]
64
+ def id
65
+ @@id
66
+ end
67
+
68
+ # Various debugging information concerning the running Ipfs daemon
69
+ # and this library
70
+ #
71
+ # @example
72
+ # Ipfs::Client.daemon
73
+ # #=> {
74
+ # :version: "0.4.13",
75
+ # commit: "cc01b7f",
76
+ # repo: "6",
77
+ # system: "amd64/darwin",
78
+ # golang: "go1.9.2"
79
+ # }
80
+ #
81
+ # @return [Hash{Symbol => String}]
82
+ def daemon
83
+ @@daemon
84
+ end
85
+
86
+ private
87
+
88
+ def call(command)
89
+ begin
90
+ @@connection.request(
91
+ command.verb,
92
+ "#{DEFAULT_BASE_PATH}#{command.path}",
93
+ command.options
94
+ )
95
+ rescue HTTP::ConnectionError
96
+ raise Ipfs::Error::UnreachableDaemon, "IPFS is not reachable."
97
+ end
98
+ end
99
+
100
+ def attempt_connection
101
+ find_up = ->(connections) {
102
+ connections.each { |connection|
103
+ co = connection.new
104
+
105
+ return co if co.up?
106
+ }
107
+ }
108
+
109
+ @@connection = find_up.call(CONNECTION_METHODS).make_persistent
110
+ end
111
+
112
+ def retrieve_ids
113
+ (execute Command::Id).tap do |ids|
114
+ @@id = {
115
+ peer_id: ids['ID'],
116
+ addresses: ids['Addresses'],
117
+ public_key: ids['PublicKey'],
118
+ agent_version: ids['AgentVersion'],
119
+ }
120
+ end
121
+ end
122
+
123
+ def retrieve_daemon_version
124
+ (execute Command::Version).tap do |version|
125
+ @@daemon = {
126
+ version: version['Version'],
127
+ commit: version['Commit'],
128
+ repo: version['Repo'],
129
+ system: version['System'],
130
+ golang: version['Golang'],
131
+ api: DEFAULT_BASE_PATH.split('/')[-1]
132
+ }
133
+ end
134
+ end
135
+ end
136
+
137
+ initialize
138
+
139
+ private_class_method :new
140
+ end
141
+ end
@@ -0,0 +1,28 @@
1
+ require 'http'
2
+ require 'uri'
3
+
4
+ module Ipfs
5
+ module Connection
6
+ class Base
7
+ DEFAULT_BASE_PATH = '/api/v0'
8
+ attr_reader :host, :port
9
+
10
+ def build_uri
11
+ URI::HTTP.build(host: @host, port: @port)
12
+ end
13
+
14
+ def up?
15
+ begin
16
+ HTTP.get("http://#{@host}:#{@port}#{DEFAULT_BASE_PATH}/id")
17
+ true
18
+ rescue HTTP::ConnectionError
19
+ false
20
+ end
21
+ end
22
+
23
+ def make_persistent
24
+ HTTP.persistent build_uri
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ require_relative './base'
2
+
3
+ module Ipfs
4
+ module Connection
5
+ class Default < Base
6
+ def initialize
7
+ @host = 'localhost'
8
+ @port = 5001
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ require_relative './base'
2
+
3
+ module Ipfs
4
+ module Connection
5
+ class IpfsConfig < Base
6
+ CONFIG_FILEPATH = "#{ENV['HOME']}/.ipfs/config"
7
+
8
+ def initialize
9
+ parse_config.tap { |location|
10
+ @host = location[:host]
11
+ @port = location[:port]
12
+ }
13
+ end
14
+
15
+ private
16
+
17
+ def parse_config
18
+ %r{.*API.*/ip4/(.*)/tcp/(\d+)}.match(::File.read CONFIG_FILEPATH) do |matched_data|
19
+ {
20
+ host: matched_data[1],
21
+ port: matched_data[2].to_i
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ require_relative './base'
2
+ require_relative '../errors'
3
+
4
+ module Ipfs
5
+ module Connection
6
+ class Unreachable < Base
7
+ def up?
8
+ raise Ipfs::Error::UnreachableDaemon, "IPFS is not reachable."
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ require_relative './errors'
2
+
3
+ module Ipfs
4
+ class DagStream
5
+ attr_reader :content
6
+
7
+ def initialize(response)
8
+ if response.status.code == 200
9
+ @content = response.body.to_s
10
+ else
11
+ raise Error::InvalidDagStream, JSON.parse(response.body)['Message']
12
+ end
13
+ end
14
+
15
+ def to_s
16
+ @content
17
+ end
18
+
19
+ alias to_str to_s
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ module Ipfs
2
+ module Error
3
+ class InvalidDagStream < StandardError
4
+ end
5
+
6
+ class InvalidMultihash < StandardError
7
+ end
8
+
9
+ class UnreachableDaemon < StandardError
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,112 @@
1
+ require_relative './multihash'
2
+ require_relative './api/files/add'
3
+ require_relative './api/files/cat'
4
+
5
+ module Ipfs
6
+ # @attr_reader [String] path The file's path.
7
+ # @attr_reader [Ipfs::Multihash] multihash The file's multihash as returned by Ipfs.
8
+ # @attr_reader [Integer] size The file's size in bytes as returned by Ipfs.
9
+ # @attr_reader [String] name The file's name as returned by Ipfs.
10
+ class File
11
+ attr_reader :path, :multihash, :size, :name
12
+
13
+ # Create an Ipfs file object, either from a Multihash or from a filepath
14
+ # allowing a file to be added to and be retrieved from Ipfs.
15
+ #
16
+ # @example given a filepath
17
+ # Ipfs::File.new(path: 'path/to/file')
18
+ # #=> #<Ipfs::File @path="path/to/file", @added=false>
19
+ # @example given a multihash
20
+ # Ipfs::File.new(multihash: 'QmVfpW2rKzzahcxt5LfYyNnnKvo1L7XyRF8Ykmhttcyztv')
21
+ # #=> #<Ipfs::File @added=false, @multihash=#<Ipfs::Multihash ....>>
22
+ #
23
+ # @param attributes [Hash{Symbol => String}]
24
+ #
25
+ # @return [Ipfs::File]
26
+ #
27
+ # @raise [Error::InvalidMultihash, Errno::ENOENT] Whether the path leads to
28
+ # a non-file entity or the multihash may be invalid,
29
+ # an error is thrown.
30
+ def initialize(**attributes)
31
+ attributes.each { |name, value|
32
+ instance_variable_set("@#{name}".to_sym, send("init_#{name}", value))
33
+ }
34
+
35
+ @added = false
36
+ end
37
+
38
+ # Add a file to the Ipfs' node.
39
+ #
40
+ # @note the call to Ipfs completes data about the added file.
41
+ # See {#multihash}, {#size} and {#name}.
42
+ #
43
+ # An {#Ipfs::File} instantiated from a multihash will not be added to Ipfs
44
+ # (as the presence of the multihash already suppose its addition to a node).
45
+ # In such case, the object is still returned but no call to Ipfs occurs.
46
+ #
47
+ # @example file not being added to Ipfs
48
+ # file = Ipfs::File.new(path: 'path/to/file')
49
+ # file.cat
50
+ # #=> ''
51
+ # file.multihash
52
+ # #=> nil
53
+ # file.name
54
+ # #=> nil
55
+ # file.size
56
+ # #=> nil
57
+ # @example file being added
58
+ # file = Ipfs::File.new(path: 'path/to/file').add
59
+ # file.cat
60
+ # #=> 'file content'
61
+ # file.multihash
62
+ # #=> #<Ipfs::Multihash ...>
63
+ # file.name
64
+ # #=> 'file'
65
+ # file.size
66
+ # #=> 20
67
+ #
68
+ # @return [Ipfs::File] Returns the object on which the method was call.
69
+ def add
70
+ tap {
71
+ Ipfs::Client.execute(Command::Add, @path).tap { |response|
72
+ @added = true
73
+
74
+ @multihash = init_multihash(response['Hash'])
75
+ @size = response['Size'].to_i
76
+ @name = response['Name']
77
+ } if !@added
78
+ }
79
+ end
80
+
81
+ # Use the {#multihash} to get the content of a file from Ipfs and returns it.
82
+ #
83
+ # @note the file must be added first or have a multihash. See {#add} and {#multihash}.
84
+ #
85
+ # @example
86
+ # Ipfs::File.new(path: 'path/to/file').add.cat
87
+ # #=> 'file content'
88
+ #
89
+ # @return [String] The content is returned.
90
+ def cat
91
+ begin
92
+ Ipfs::Client.execute(Command::Cat, @multihash).to_s if @multihash
93
+ rescue Ipfs::Error::InvalidDagStream
94
+ ''
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def init_multihash(multihash)
101
+ multihash.is_a?(Multihash) ? multihash : Multihash.new(multihash)
102
+ end
103
+
104
+ def init_path(path)
105
+ if ::File.file? path
106
+ path
107
+ else
108
+ raise Errno::ENOENT, 'no such file or directory'
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,53 @@
1
+ require_relative './base'
2
+ require_relative './errors'
3
+
4
+ module Ipfs
5
+ class Multihash
6
+ attr_reader :hash_func_type, :digest_length
7
+
8
+ FUNCTIONS = [
9
+ { name: :sha256, type_code: 0x12, digest_length: 0x20 }
10
+ ]
11
+
12
+ def initialize(multihash)
13
+ @base58_encoded = multihash
14
+
15
+ raise Error::InvalidMultihash,
16
+ "The hash '#{@base58_encoded}' is invalid." unless @base58_encoded.is_a?(String)
17
+
18
+ @bytes_encoded = to_bytes
19
+
20
+ @function = find_hash_function(@bytes_encoded[0])
21
+
22
+ raise Error::InvalidMultihash, "The hash func type could not be found" if @function.nil?
23
+
24
+ @hash_func_type = @function[:name]
25
+ @digest_length = @function[:digest_length]
26
+
27
+ raise Error::InvalidMultihash,
28
+ "The hash '#{@base58_encoded}' is invalid." unless correct_length?
29
+ end
30
+
31
+ def to_bytes
32
+ [Base58.decode(@base58_encoded).to_s(16)]
33
+ .pack('H*')
34
+ .unpack('C*')
35
+ end
36
+
37
+ def raw
38
+ @base58_encoded
39
+ end
40
+
41
+ alias to_s raw
42
+
43
+ private
44
+
45
+ def find_hash_function(func_type_code)
46
+ FUNCTIONS.find { |function| function[:type_code] == func_type_code }
47
+ end
48
+
49
+ def correct_length?
50
+ @digest_length == @bytes_encoded[2..-1].length
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ require_relative './request'
2
+
3
+ module Ipfs
4
+ class BasicRequest < Request
5
+ def initialize(path, **arguments)
6
+ super(path, :get)
7
+
8
+ @multihash = arguments[:multihash]
9
+ end
10
+
11
+ def options
12
+ @multihash \
13
+ ? { params: { arg: @multihash.raw } } \
14
+ : {}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ require_relative './request'
2
+
3
+ module Ipfs
4
+ class FileUploadRequest < Request
5
+ def initialize(path, filepath)
6
+ super(path, :post)
7
+
8
+ @filepath = filepath
9
+ end
10
+
11
+ def options
12
+ {
13
+ form: {
14
+ arg: HTTP::FormData::File.new(@filepath)
15
+ }
16
+ }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Ipfs
2
+ class Request
3
+ attr_reader :path, :verb
4
+
5
+ def initialize(path, verb)
6
+ @path = path
7
+ @verb = verb
8
+ end
9
+
10
+ def options
11
+ {}
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Ipfs
2
+ VERSION = '0.5.1'
3
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-ipfs-http-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.1
5
+ platform: ruby
6
+ authors:
7
+ - Tom Benett
8
+ - Neil Nilou
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-02-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 3.8.0
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 3.8.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: webmock
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 3.5.1
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 3.5.1
42
+ description: A client library for the IPFS HTTP API, implemented in Ruby
43
+ email: tom@benett.io
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/ipfs.rb
49
+ - lib/ruby-ipfs-http-client/api/command.rb
50
+ - lib/ruby-ipfs-http-client/api/files/add.rb
51
+ - lib/ruby-ipfs-http-client/api/files/cat.rb
52
+ - lib/ruby-ipfs-http-client/api/files/ls.rb
53
+ - lib/ruby-ipfs-http-client/api/generic/id.rb
54
+ - lib/ruby-ipfs-http-client/api/generic/version.rb
55
+ - lib/ruby-ipfs-http-client/base.rb
56
+ - lib/ruby-ipfs-http-client/client.rb
57
+ - lib/ruby-ipfs-http-client/connection/base.rb
58
+ - lib/ruby-ipfs-http-client/connection/default.rb
59
+ - lib/ruby-ipfs-http-client/connection/ipfs_config.rb
60
+ - lib/ruby-ipfs-http-client/connection/unreachable.rb
61
+ - lib/ruby-ipfs-http-client/dagstream.rb
62
+ - lib/ruby-ipfs-http-client/errors.rb
63
+ - lib/ruby-ipfs-http-client/file.rb
64
+ - lib/ruby-ipfs-http-client/multihash.rb
65
+ - lib/ruby-ipfs-http-client/request/basic_request.rb
66
+ - lib/ruby-ipfs-http-client/request/file_upload_request.rb
67
+ - lib/ruby-ipfs-http-client/request/request.rb
68
+ - lib/ruby-ipfs-http-client/version.rb
69
+ homepage: https://github.com/tbenett/ruby-ipfs-http-client
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '2.4'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.0.1
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: ipfs client
92
+ test_files: []