From 4fedf0edede459106ffcce1ab56cba4700f80880 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone <exarkun@twistedmatrix.com> Date: Sat, 19 Feb 2022 11:17:54 -0500 Subject: [PATCH] add the ability to make directories, link entries, and download them --- src/_zkapauthorizer/tahoe.py | 60 +++++++++++++++-- src/_zkapauthorizer/tests/test_tahoe.py | 89 +++++++++++++++++++++++-- 2 files changed, 140 insertions(+), 9 deletions(-) diff --git a/src/_zkapauthorizer/tahoe.py b/src/_zkapauthorizer/tahoe.py index 60d696e..549de1f 100644 --- a/src/_zkapauthorizer/tahoe.py +++ b/src/_zkapauthorizer/tahoe.py @@ -4,7 +4,7 @@ A library for interacting with a Tahoe-LAFS node. from collections.abc import Awaitable from functools import wraps -from typing import Callable, List +from typing import Callable, Iterable, List, Optional import treq from attrs import define @@ -59,6 +59,8 @@ class TahoeAPIError(Exception): :ivar body: The HTTP response body. """ + method: str + url: DecodedURL status: int body: str @@ -94,11 +96,15 @@ async def upload( content = (await treq.content(resp)).decode("utf-8") if resp.code in (200, 201): return content - raise TahoeAPIError(resp.code, content) + raise TahoeAPIError("put", uri, resp.code, content) async def download( - client: HTTPClient, outpath: FilePath, api_root: DecodedURL, cap: str + client: HTTPClient, + outpath: FilePath, + api_root: DecodedURL, + cap: str, + child_path: Optional[Iterable[str]] = None, ) -> Awaitable: # Awaitable[None] but this requires Python 3.9 """ Download the object identified by the given capability to the given path. @@ -120,11 +126,55 @@ async def download( """ outtemp = outpath.temporarySibling() - resp = await client.get(api_root.child("uri", cap).to_text()) + uri = api_root.child("uri").child(cap) + if child_path is not None: + for segment in child_path: + uri = uri.child(segment) + + resp = await client.get(uri) if resp.code == 200: with outtemp.open("w") as f: await treq.collect(resp, f.write) outtemp.moveTo(outpath) else: content = (await treq.content(resp)).decode("utf-8") - raise TahoeAPIError(resp.code, content) + raise TahoeAPIError("get", uri, resp.code, content) + + +async def make_directory( + client: HTTPClient, + api_root: DecodedURL, +) -> Awaitable: # Awaitable[str] but this requires Python 3.9 + """ + Create a new mutable directory and return the write capability string. + """ + uri = api_root.child("uri").add("t", "mkdir") + resp = await client.post(uri) + content = (await treq.content(resp)).decode("utf-8") + if resp.code == 200: + return content + raise TahoeAPIError("post", uri, resp.code, content) + + +async def link( + client: HTTPClient, + api_root: DecodedURL, + dir_cap: str, + entry_name: str, + entry_cap: str, +) -> Awaitable: + """ + Link an object into a directory. + + :param dir_cap: The capability string of the directory in which to create + the link. + + :param entry_cap: The capability string of the object to link in to the + directory. + """ + uri = api_root.child("uri").child(dir_cap).child(entry_name).add("t", "uri") + resp = await client.put(uri, data=entry_cap.encode("ascii")) + content = (await treq.content(resp)).decode("utf-8") + if resp.code == 200: + return None + raise TahoeAPIError("put", uri, resp.code, content) diff --git a/src/_zkapauthorizer/tests/test_tahoe.py b/src/_zkapauthorizer/tests/test_tahoe.py index 0efdfd0..3564bc4 100644 --- a/src/_zkapauthorizer/tests/test_tahoe.py +++ b/src/_zkapauthorizer/tests/test_tahoe.py @@ -14,13 +14,13 @@ from fixtures import TempDir from hyperlink import DecodedURL from testresources import TestResourceManager, setUpResources, tearDownResources from testtools import TestCase -from testtools.matchers import Equals, Is, raises +from testtools.matchers import Equals, Is, Not, raises from testtools.twistedsupport import AsynchronousDeferredRunTest -from twisted.internet.defer import ensureDeferred, inlineCallbacks +from twisted.internet.defer import Deferred, ensureDeferred, inlineCallbacks from twisted.python.filepath import FilePath from yaml import safe_dump -from ..tahoe import async_retry, download, upload +from ..tahoe import async_retry, download, link, make_directory, upload from .fixtures import Treq # A plausible value for the ``retry`` parameter of ``wait_for_path``. @@ -299,6 +299,9 @@ class TahoeClientManager(TestResourceManager): return client +_client_manager = TahoeClientManager() + + class UploadDownloadTestCase(TestCase): """ Tests for ``upload`` and ``download``. @@ -308,7 +311,7 @@ class UploadDownloadTestCase(TestCase): run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=60.0) # Get a Tahoe-LAFS client node connected to a storage node. - resources = [("client", TahoeClientManager())] + resources = [("client", _client_manager)] def setUp(self): super().setUp() @@ -339,6 +342,84 @@ class UploadDownloadTestCase(TestCase): ) +class DirectoryTests(TestCase): + """ + Tests for directory-related functionality. + """ + + # Support test methods that return a Deferred. + run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=60.0) + + # Get a Tahoe-LAFS client node connected to a storage node. + resources = [("client", _client_manager)] + + def setUp(self): + super().setUp() + setUpResources(self, self.resources, None) + self.addCleanup(lambda: tearDownResources(self, self.resources, None)) + # AsynchronousDeferredRunTest sets reactor on us. + self.httpclient = self.useFixture(Treq(self.reactor, case=self)).client() + + @inlineCallbacks + def test_make_directory(self): + """ + ``make_directory`` returns a coroutine that completes with the capability + of a new, empty directory. + """ + dir_cap = yield Deferred.fromCoroutine( + make_directory(self.httpclient, self.client.node_url) + ) + + # If we can download it, consider that success. + outpath = FilePath(self.useFixture(TempDir()).join("dir_contents")) + yield Deferred.fromCoroutine( + download(self.httpclient, outpath, self.client.node_url, dir_cap) + ) + self.assertThat(outpath.getContent(), Not(Equals(b""))) + + @inlineCallbacks + def test_link(self): + """ + ``link`` adds an entry to a directory. + """ + tmp = FilePath(self.useFixture(TempDir()).path) + inpath = tmp.child("source") + inpath.setContent(b"some content") + + dir_cap = yield Deferred.fromCoroutine( + make_directory(self.httpclient, self.client.node_url) + ) + entry_name = "foo" + entry_cap = yield Deferred.fromCoroutine( + upload(self.httpclient, inpath, self.client.node_url), + ) + yield Deferred.fromCoroutine( + link( + self.httpclient, + self.client.node_url, + dir_cap, + entry_name, + entry_cap, + ), + ) + + outpath = tmp.child("destination") + yield Deferred.fromCoroutine( + download( + self.httpclient, + outpath, + self.client.node_url, + dir_cap, + child_path=[entry_name], + ), + ) + + self.assertThat( + outpath.getContent(), + Equals(inpath.getContent()), + ) + + class AsyncRetryTests(TestCase): """ Tests for ``async_retry``. -- GitLab