From f5437d9be73b42b891f892336551c95074d84b4d Mon Sep 17 00:00:00 2001
From: Itamar Turner-Trauring <itamar@itamarst.org>
Date: Wed, 12 Jan 2022 11:51:56 -0500
Subject: [PATCH] Some progress towards bucket allocation endpoint, and
 defining the protocol better.

---
 docs/proposed/http-storage-node-protocol.rst |  5 +++
 src/allmydata/storage/http_client.py         | 12 ++++++--
 src/allmydata/storage/http_server.py         | 32 ++++++++++++++++----
 3 files changed, 40 insertions(+), 9 deletions(-)

diff --git a/docs/proposed/http-storage-node-protocol.rst b/docs/proposed/http-storage-node-protocol.rst
index a8555cd26..26f1a2bb7 100644
--- a/docs/proposed/http-storage-node-protocol.rst
+++ b/docs/proposed/http-storage-node-protocol.rst
@@ -382,6 +382,11 @@ the server will respond with ``400 BAD REQUEST``.
 
 If authorization using the secret fails, then a ``401 UNAUTHORIZED`` response should be sent.
 
+Encoding
+~~~~~~~~
+
+* ``storage_index`` should be base32 encoded (RFC3548) in URLs.
+
 General
 ~~~~~~~
 
diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py
index e38525583..5e964bfbe 100644
--- a/src/allmydata/storage/http_client.py
+++ b/src/allmydata/storage/http_client.py
@@ -34,6 +34,12 @@ from hyperlink import DecodedURL
 import treq
 
 from .http_common import swissnum_auth_header, Secrets
+from .common import si_b2a
+
+
+def _encode_si(si):  # type: (bytes) -> str
+    """Encode the storage index into Unicode string."""
+    return str(si_b2a(si), "ascii")
 
 
 class ClientException(Exception):
@@ -151,7 +157,7 @@ class StorageClientImmutables(object):
         Result fires when creating the storage index succeeded, if creating the
         storage index failed the result will fire with an exception.
         """
-        url = self._client._url("/v1/immutable/" + str(storage_index, "ascii"))
+        url = self._client._url("/v1/immutable/" + _encode_si(storage_index))
         message = dumps(
             {"share-numbers": share_numbers, "allocated-size": allocated_size}
         )
@@ -189,7 +195,7 @@ class StorageClientImmutables(object):
         been uploaded.
         """
         url = self._client._url(
-            "/v1/immutable/{}/{}".format(str(storage_index, "ascii"), share_number)
+            "/v1/immutable/{}/{}".format(_encode_si(storage_index), share_number)
         )
         response = yield self._client._request(
             "POST",
@@ -238,7 +244,7 @@ class StorageClientImmutables(object):
         https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3777
         """
         url = self._client._url(
-            "/v1/immutable/{}/{}".format(str(storage_index, "ascii"), share_number)
+            "/v1/immutable/{}/{}".format(_encode_si(storage_index), share_number)
         )
         response = yield self._client._request(
             "GET",
diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py
index b371fc395..23f0d2f1c 100644
--- a/src/allmydata/storage/http_server.py
+++ b/src/allmydata/storage/http_server.py
@@ -28,6 +28,7 @@ from cbor2 import dumps, loads
 
 from .server import StorageServer
 from .http_common import swissnum_auth_header, Secrets
+from .common import si_a2b
 from .immutable import BucketWriter
 from ..util.hashutil import timing_safe_compare
 
@@ -174,6 +175,7 @@ class HTTPServer(object):
     )
     def allocate_buckets(self, request, authorization, storage_index):
         """Allocate buckets."""
+        storage_index = si_a2b(storage_index.encode("ascii"))
         info = loads(request.content.read())
         upload_key = authorization[Secrets.UPLOAD]
 
@@ -191,13 +193,29 @@ class HTTPServer(object):
                 pass
         else:
             # New upload.
-            # TODO self._storage_server.allocate_buckets() with given inputs.
-            # TODO add results to self._uploads.
-            pass
+            already_got, sharenum_to_bucket = self._storage_server.allocate_buckets(
+                storage_index,
+                renew_secret=authorization[Secrets.LEASE_RENEW],
+                cancel_secret=authorization[Secrets.LEASE_CANCEL],
+                sharenums=info["share-numbers"],
+                allocated_size=info["allocated-size"],
+            )
+            self._uploads[storage_index] = StorageIndexUploads(
+                shares=sharenum_to_bucket, upload_key=authorization[Secrets.UPLOAD]
+            )
+            return self._cbor(
+                request,
+                {
+                    "already-have": set(already_got),
+                    "allocated": set(sharenum_to_bucket),
+                },
+            )
 
     @_authorized_route(
         _app,
-        {Secrets.UPLOAD}, "/v1/immutable/<string:storage_index>/<int:share_number>", methods=["PATCH"]
+        {Secrets.UPLOAD},
+        "/v1/immutable/<string:storage_index>/<int:share_number>",
+        methods=["PATCH"],
     )
     def write_share_data(self, request, authorization, storage_index, share_number):
         """Write data to an in-progress immutable upload."""
@@ -212,7 +230,10 @@ class HTTPServer(object):
         # TODO if it finished writing altogether, 201 CREATED. Otherwise 200 OK.
 
     @_authorized_route(
-        _app, set(), "/v1/immutable/<string:storage_index>/<int:share_number>", methods=["GET"]
+        _app,
+        set(),
+        "/v1/immutable/<string:storage_index>/<int:share_number>",
+        methods=["GET"],
     )
     def read_share_chunk(self, request, authorization, storage_index, share_number):
         """Read a chunk for an already uploaded immutable."""
@@ -221,4 +242,3 @@ class HTTPServer(object):
         # TODO lookup the share
         # TODO if not found, 404
         # TODO otherwise, return data from that offset
-