From 715e4bc4b318e72a83413ba821389bbbd3194473 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Tue, 4 Feb 2020 01:00:11 -0800 Subject: [PATCH] add IPFS loader support.. use cloudflare instead of the ipfs.io one because the IPv6 address seems to hang often right now.. --- casimport/__init__.py | 72 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/casimport/__init__.py b/casimport/__init__.py index bd39908..892d9f6 100644 --- a/casimport/__init__.py +++ b/casimport/__init__.py @@ -23,6 +23,7 @@ # SUCH DAMAGE. import contextlib +import functools import glob import hashlib import importlib.resources @@ -32,11 +33,27 @@ import pathlib import shutil import sys import tempfile -import urllib +import urllib.request from importlib.abc import MetaPathFinder, Loader from importlib.machinery import ModuleSpec +def _printanyexc(f): + '''Prints any exception that gets raised by the wrapped function.''' + + @functools.wraps(f) + def wrapper(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception: + import traceback + + traceback.print_exc() + + raise + + return wrapper + @contextlib.contextmanager def tempset(obj, key, value): '''A context (with) manager for changing the value of an item in a @@ -88,6 +105,26 @@ def tempattrset(obj, key, value): else: delattr(obj, key) +class IPFSCAS(object): + gwhost = 'gateway.ipfs.io' + gwhost = 'cloudflare-ipfs.com' + + def make_url(self, url): + return urllib.parse.urlunparse(('https', self.gwhost, + '/ipfs/' + url.netloc) + ('', ) * 3) + + def fetch_data(self, url): + if url.scheme != 'ipfs': + raise ValueError('cannot handle scheme %s' % + repr(url.scheme)) + gwurl = self.make_url(url) + + with urllib.request.urlopen(gwurl) as req: + if req.status // 100 != 2: + raise RuntimeError('bad fetch') + + return req.read() + class FileDirCAS(object): '''A file loader for CAS that operates on a directory. It looks at files, caches their hash, and loads them upon request.''' @@ -296,6 +333,7 @@ def defaultinit(casf): cachedir.mkdir(exist_ok=True) casf.register(FileDirCAS(cachedir)) + casf.register(IPFSCAS()) # The global version _casfinder = CASFinder() @@ -423,6 +461,9 @@ class Test(unittest.TestCase): # it can be imported from cas.v1_f_330884aa2febb5e19fb7194ec6a69ed11dd3d77122f1a5175ee93e73cf0161c3 import hello + # and that the last loader is the IPFSCAS + self.assertIsInstance(f._loaders[-1], IPFSCAS) + with CASFinder() as f: defaultinit(f) @@ -514,6 +555,35 @@ class Test(unittest.TestCase): finally: sys.path.remove(fixdir) + @mock.patch('urllib.request.urlopen') + def test_ipfscasloader(self, uomock): + # prep return test data + with open(self.fixtures / 'hello.py') as fp: + # that returns the correct data + ipfsdata = fp.read() + + # that the ipfs CAS loader + ipfs = IPFSCAS() + + # that the request is successfull + uomock.return_value.__enter__.return_value.status = 200 + + # and returns the correct data + uomock.return_value.__enter__.return_value.read.return_value = ipfsdata + + # that when called + hashurl = urllib.parse.urlparse('ipfs://bafkreibtbcckul7lwxqz7nyzj3dknhwrdxj5o4jc6gsroxxjhzz46albym') + data = ipfs.fetch_data(hashurl) + + # it opens the correct url + uomock.assert_called_with('https://cloudflare-ipfs.com/ipfs/bafkreibtbcckul7lwxqz7nyzj3dknhwrdxj5o4jc6gsroxxjhzz46albym') + + # and returns the correct data + self.assertEqual(data, ipfsdata) + + with self.assertRaises(ValueError): + ipfs.fetch_data(urllib.parse.urlparse('hash://sha256/asldfkj')) + def test_overlappingaliases(self): # make sure that an aliases file is consistent and does not # override other urls. That is that any hashes are