Skip to content

Commit 26d3b23

Browse files
committed
1 parent d898ce6 commit 26d3b23

File tree

8 files changed

+2743
-2409
lines changed

8 files changed

+2743
-2409
lines changed

ipfsapi/client.py

-2,409
This file was deleted.

ipfsapi/client/__init__.py

+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# -*- coding: utf-8 -*-
2+
"""IPFS API Bindings for Python.
3+
4+
Classes:
5+
6+
* Client – a TCP client for interacting with an IPFS daemon
7+
"""
8+
from __future__ import absolute_import
9+
10+
import os
11+
import warnings
12+
13+
DEFAULT_HOST = str(os.environ.get("PY_IPFSAPI_DEFAULT_HOST", 'localhost'))
14+
DEFAULT_PORT = int(os.environ.get("PY_IPFSAPI_DEFAULT_PORT", 5001))
15+
DEFAULT_BASE = str(os.environ.get("PY_IPFSAPI_DEFAULT_BASE", 'api/v0'))
16+
17+
VERSION_MINIMUM = "0.4.3"
18+
VERSION_MAXIMUM = "0.5.0"
19+
20+
from . import files
21+
from . import graph
22+
from . import crypto
23+
from . import network
24+
from . import node
25+
26+
import ipfsapi.multipart
27+
from .. import utils, exceptions, encoding
28+
29+
30+
def assert_version(version, minimum=VERSION_MINIMUM, maximum=VERSION_MAXIMUM):
31+
"""Make sure that the given daemon version is supported by this client
32+
version.
33+
34+
Raises
35+
------
36+
~ipfsapi.exceptions.VersionMismatch
37+
38+
Parameters
39+
----------
40+
version : str
41+
The version of an IPFS daemon.
42+
minimum : str
43+
The minimal IPFS version to allow.
44+
maximum : str
45+
The maximum IPFS version to allow.
46+
"""
47+
# Convert version strings to integer tuples
48+
version = list(map(int, version.split('-', 1)[0].split('.')))
49+
minimum = list(map(int, minimum.split('-', 1)[0].split('.')))
50+
maximum = list(map(int, maximum.split('-', 1)[0].split('.')))
51+
52+
if minimum > version or version >= maximum:
53+
raise exceptions.VersionMismatch(version, minimum, maximum)
54+
55+
56+
def connect(host=DEFAULT_HOST, port=DEFAULT_PORT, base=DEFAULT_BASE,
57+
chunk_size=ipfsapi.multipart.default_chunk_size, **defaults):
58+
"""Create a new :class:`~ipfsapi.Client` instance and connect to the
59+
daemon to validate that its version is supported.
60+
61+
Raises
62+
------
63+
~ipfsapi.exceptions.VersionMismatch
64+
~ipfsapi.exceptions.ErrorResponse
65+
~ipfsapi.exceptions.ConnectionError
66+
~ipfsapi.exceptions.ProtocolError
67+
~ipfsapi.exceptions.StatusError
68+
~ipfsapi.exceptions.TimeoutError
69+
70+
71+
All parameters are identical to those passed to the constructor of the
72+
:class:`~ipfsapi.Client` class.
73+
74+
Returns
75+
-------
76+
~ipfsapi.Client
77+
"""
78+
# Create client instance
79+
client = Client(host, port, base, chunk_size, **defaults)
80+
81+
# Query version number from daemon and validate it
82+
assert_version(client.version()['Version'])
83+
84+
return client
85+
86+
87+
class Client(
88+
files.FilesBase,
89+
graph.GraphBase,
90+
crypto.CryptoBase,
91+
network.NetworkBase,
92+
node.NodeBase
93+
):
94+
def dns(self, domain_name, recursive=False, **kwargs):
95+
"""Resolves DNS links to the referenced object.
96+
97+
Multihashes are hard to remember, but domain names are usually easy to
98+
remember. To create memorable aliases for multihashes, DNS TXT records
99+
can point to other DNS links, IPFS objects, IPNS keys, etc.
100+
This command resolves those links to the referenced object.
101+
102+
For example, with this DNS TXT record::
103+
104+
>>> import dns.resolver
105+
>>> a = dns.resolver.query("ipfs.io", "TXT")
106+
>>> a.response.answer[0].items[0].to_text()
107+
'"dnslink=/ipfs/QmTzQ1JRkWErjk39mryYw2WVaphAZNAREyMchXzYQ7c15n"'
108+
109+
The resolver will give::
110+
111+
>>> c.dns("ipfs.io")
112+
{'Path': '/ipfs/QmTzQ1JRkWErjk39mryYw2WVaphAZNAREyMchXzYQ7c15n'}
113+
114+
Parameters
115+
----------
116+
domain_name : str
117+
The domain-name name to resolve
118+
recursive : bool
119+
Resolve until the name is not a DNS link
120+
121+
Returns
122+
-------
123+
dict : Resource were a DNS entry points to
124+
"""
125+
kwargs.setdefault("opts", {"recursive": recursive})
126+
127+
args = (domain_name,)
128+
return self._client.request('/dns', args, decoder='json', **kwargs)
129+
130+
###########
131+
# HELPERS #
132+
###########
133+
134+
@utils.return_field('Hash')
135+
def add_bytes(self, data, **kwargs):
136+
"""Adds a set of bytes as a file to IPFS.
137+
138+
.. code-block:: python
139+
140+
>>> c.add_bytes(b"Mary had a little lamb")
141+
'QmZfF6C9j4VtoCsTp4KSrhYH47QMd3DNXVZBKaxJdhaPab'
142+
143+
Also accepts and will stream generator objects.
144+
145+
Parameters
146+
----------
147+
data : bytes
148+
Content to be added as a file
149+
150+
Returns
151+
-------
152+
str : Hash of the added IPFS object
153+
"""
154+
body, headers = ipfsapi.multipart.stream_bytes(data, self.chunk_size)
155+
return self._client.request('/add', decoder='json',
156+
data=body, headers=headers, **kwargs)
157+
158+
@utils.return_field('Hash')
159+
def add_str(self, string, **kwargs):
160+
"""Adds a Python string as a file to IPFS.
161+
162+
.. code-block:: python
163+
164+
>>> c.add_str(u"Mary had a little lamb")
165+
'QmZfF6C9j4VtoCsTp4KSrhYH47QMd3DNXVZBKaxJdhaPab'
166+
167+
Also accepts and will stream generator objects.
168+
169+
Parameters
170+
----------
171+
string : str
172+
Content to be added as a file
173+
174+
Returns
175+
-------
176+
str : Hash of the added IPFS object
177+
"""
178+
body, headers = ipfsapi.multipart.stream_text(string, self.chunk_size)
179+
return self._client.request('/add', decoder='json',
180+
data=body, headers=headers, **kwargs)
181+
182+
def add_json(self, json_obj, **kwargs):
183+
"""Adds a json-serializable Python dict as a json file to IPFS.
184+
185+
.. code-block:: python
186+
187+
>>> c.add_json({'one': 1, 'two': 2, 'three': 3})
188+
'QmVz9g7m5u3oHiNKHj2CJX1dbG1gtismRS3g9NaPBBLbob'
189+
190+
Parameters
191+
----------
192+
json_obj : dict
193+
A json-serializable Python dictionary
194+
195+
Returns
196+
-------
197+
str : Hash of the added IPFS object
198+
"""
199+
return self.add_bytes(encoding.Json().encode(json_obj), **kwargs)
200+
201+
def get_json(self, multihash, **kwargs):
202+
"""Loads a json object from IPFS.
203+
204+
.. code-block:: python
205+
206+
>>> c.get_json('QmVz9g7m5u3oHiNKHj2CJX1dbG1gtismRS3g9NaPBBLbob')
207+
{'one': 1, 'two': 2, 'three': 3}
208+
209+
Parameters
210+
----------
211+
multihash : str
212+
Multihash of the IPFS object to load
213+
214+
Returns
215+
-------
216+
object : Deserialized IPFS JSON object value
217+
"""
218+
return self.cat(multihash, decoder='json', **kwargs)
219+
220+
def add_pyobj(self, py_obj, **kwargs):
221+
"""Adds a picklable Python object as a file to IPFS.
222+
223+
.. deprecated:: 0.4.2
224+
The ``*_pyobj`` APIs allow for arbitrary code execution if abused.
225+
Either switch to :meth:`~ipfsapi.Client.add_json` or use
226+
``client.add_bytes(pickle.dumps(py_obj))`` instead.
227+
228+
Please see :meth:`~ipfsapi.Client.get_pyobj` for the
229+
**security risks** of using these methods!
230+
231+
.. code-block:: python
232+
233+
>>> c.add_pyobj([0, 1.0, 2j, '3', 4e5])
234+
'QmWgXZSUTNNDD8LdkdJ8UXSn55KfFnNvTP1r7SyaQd74Ji'
235+
236+
Parameters
237+
----------
238+
py_obj : object
239+
A picklable Python object
240+
241+
Returns
242+
-------
243+
str : Hash of the added IPFS object
244+
"""
245+
warnings.warn("Using `*_pyobj` on untrusted data is a security risk",
246+
DeprecationWarning)
247+
return self.add_bytes(encoding.Pickle().encode(py_obj), **kwargs)
248+
249+
def get_pyobj(self, multihash, **kwargs):
250+
"""Loads a pickled Python object from IPFS.
251+
252+
.. deprecated:: 0.4.2
253+
The ``*_pyobj`` APIs allow for arbitrary code execution if abused.
254+
Either switch to :meth:`~ipfsapi.Client.get_json` or use
255+
``pickle.loads(client.cat(multihash))`` instead.
256+
257+
.. caution::
258+
259+
The pickle module is not intended to be secure against erroneous or
260+
maliciously constructed data. Never unpickle data received from an
261+
untrusted or unauthenticated source.
262+
263+
Please **read**
264+
`this article <https://www.cs.uic.edu/%7Es/musings/pickle/>`_ to
265+
understand the security risks of using this method!
266+
267+
.. code-block:: python
268+
269+
>>> c.get_pyobj('QmWgXZSUTNNDD8LdkdJ8UXSn55KfFnNvTP1r7SyaQd74Ji')
270+
[0, 1.0, 2j, '3', 400000.0]
271+
272+
Parameters
273+
----------
274+
multihash : str
275+
Multihash of the IPFS object to load
276+
277+
Returns
278+
-------
279+
object : Deserialized IPFS Python object
280+
"""
281+
warnings.warn("Using `*_pyobj` on untrusted data is a security risk",
282+
DeprecationWarning)
283+
return self.cat(multihash, decoder='pickle', **kwargs)

ipfsapi/client/base.py

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import absolute_import
3+
4+
import warnings
5+
6+
import ipfsapi.http
7+
import ipfsapi.multipart
8+
9+
from . import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_BASE
10+
11+
12+
class SectionProperty(object):
13+
def __init__(self, cls):
14+
self.cls = cls
15+
self.instance = None
16+
17+
def __get__(self, client_object, type=None):
18+
if not self.instance:
19+
self.instance = self.cls(client_object)
20+
21+
return self.instance
22+
23+
24+
class SectionBase(object):
25+
# Accept parent object from property descriptor
26+
def __init__(self, parent):
27+
self.__parent = parent
28+
29+
# Proxy the parent's properties
30+
@property
31+
def _client(self):
32+
return self.__parent._client
33+
34+
@property
35+
def chunk_size(self):
36+
return self.__parent.chunk_size
37+
38+
@chunk_size.setter
39+
def chunk_size(self, value):
40+
self.__parent.chunk_size = value
41+
42+
43+
class ClientBase(object):
44+
"""A TCP client for interacting with an IPFS daemon.
45+
46+
A :class:`~ipfsapi.Client` instance will not actually establish a
47+
connection to the daemon until at least one of it's methods is called.
48+
49+
Parameters
50+
----------
51+
host : str
52+
Hostname or IP address of the computer running the ``ipfs daemon``
53+
node (defaults to the local system)
54+
port : int
55+
The API port of the IPFS deamon (usually 5001)
56+
base : str
57+
Path of the deamon's API (currently always ``api/v0``)
58+
chunk_size : int
59+
The size of the chunks to break uploaded files and text content into
60+
"""
61+
62+
_clientfactory = ipfsapi.http.HTTPClient
63+
64+
def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT,
65+
base=DEFAULT_BASE, chunk_size=ipfsapi.multipart.default_chunk_size,
66+
**defaults):
67+
"""Connects to the API port of an IPFS node."""
68+
69+
self.chunk_size = chunk_size
70+
71+
self._client = self._clientfactory(host, port, base, **defaults)
72+
73+
74+
class DeprecatedMethodProperty(object):
75+
def __init__(self, *path, **kwargs):
76+
#PY2: No support for kw-only parameters after glob parameters
77+
prefix = kwargs.pop("prefix", [])
78+
strip = kwargs.pop("strip", 0)
79+
assert not kwargs
80+
81+
self.props = path
82+
self.path = tuple(prefix) + (path[:-strip] if strip > 0 else tuple(path))
83+
self.warned = False
84+
85+
self.__help__ = "Deprecated method: Please use “client.{0}” instead".format(
86+
".".join(self.path)
87+
)
88+
89+
def __get__(self, obj, type=None):
90+
if not self.warned:
91+
message = "IPFS API function “{0}” has been renamed to “{1}”".format(
92+
"_".join(self.path), ".".join(self.path)
93+
)
94+
warnings.warn(message, FutureWarning)
95+
self.warned = True
96+
print()
97+
for name in self.props:
98+
print(name, obj)
99+
obj = getattr(obj, name)
100+
return obj

0 commit comments

Comments
 (0)