aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src/meshbay_common/protocol.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-common/src/meshbay_common/protocol.py')
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py40
1 files changed, 40 insertions, 0 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
index e092353..5bd2903 100644
--- a/packages/meshbay-common/src/meshbay_common/protocol.py
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -96,6 +96,20 @@ class MNP:
# never serve the GEK in plaintext. Members obtain it by unwrapping their own
# ECIES bundle. The constants lingered after the handlers were deleted, leaving
# the wire contract looking as though the endpoint still existed.
+ # Transfer slots. A download is otherwise invisible to the node -- a series
+ # of independent file_req messages, with nothing saying one started or
+ # ended -- so there is nothing to count and nothing to cap. The lease is
+ # that missing object: `tr` is drawn by the client like `upload_id`, covers
+ # a job rather than a file, and dies with the connection.
+ #
+ # One reply type with a state field, not four: a client that must switch on
+ # the message type to discover it is still waiting is a client that will get
+ # one branch wrong. Carries no filename and no path -- `tr` is opaque,
+ # `bytes` and `chunks` are numbers -- so it stays in clear like
+ # INDEX_PROGRESS, for the same stated reason.
+ TRANSFER_OPEN = "transfer_open" # client -> node: I want a slot
+ TRANSFER_CLOSE = "transfer_close" # client -> node: I am done with it
+ TRANSFER_STATE = "transfer_state" # node -> client: granted/queued/closed
FILE_UPLOAD = "file_upload" # client pushes file chunk to node
FILE_UPLOAD_ACK = "file_upload_ack" # node acknowledges chunk receipt
DIR_CREATE = "dir_create" # client → node: make a directory
@@ -138,6 +152,8 @@ class MNP:
MEMBER_UPLOAD_ACK = "member_upload_ack"
APPS_ENABLED = "apps_enabled" # operator → node: which group apps to show
APPS_ENABLED_ACK = "apps_enabled_ack"
+ TRANSFER_LIMITS = "transfer_limits" # operator → node: per-member caps for this group
+ TRANSFER_LIMITS_ACK = "transfer_limits_ack" # node → this group: the new caps
SET_SCAN_SETTINGS = "set_scan_settings" # operator → node: reconcile/debounce timing
SET_SCAN_SETTINGS_ACK = "set_scan_settings_ack"
MEDIA_META_REQ = "media_meta_req" # client → node: TMDB metadata for a path
@@ -485,6 +501,22 @@ def file_upload_payload(gek: bytes, group_id: str, msg: dict) -> dict:
return unseal(gek, PURPOSE_UPLOAD, MNP.FILE_UPLOAD, group_id, msg)
+# "Where am I?", asked as an ordinary sealed upload chunk rather than as a new
+# message.
+#
+# The node identifies an upload by (member, directory, filename), so a client
+# resuming one has to name the file — and `transfer_open`, the obvious place to
+# ask, travels in clear. Naming it there would undo exactly what sealing the
+# upload path bought: before MNP 2.0 the same file was ciphertext leaving a node
+# and plaintext arriving at one.
+#
+# So the question is asked inside the seal that already exists, as a chunk with
+# no bytes and this index. The node writes nothing, changes nothing, and answers
+# with `resume_from`. A node that predates this refuses the index, which the
+# client reads as "start from the beginning" — the behaviour it had anyway.
+UPLOAD_PROBE_INDEX = -1
+
+
def file_upload_ack_wire(
gek: bytes,
group_id: str,
@@ -494,6 +526,7 @@ def file_upload_ack_wire(
filename: str,
stored_as: str,
dir: str = "",
+ resume_from: int | None = None,
) -> dict:
"""
The node's answer to one chunk, sealed the same way.
@@ -502,8 +535,15 @@ def file_upload_ack_wire(
replacing anything — and `dir` is where it landed. Both name the operator's
content, so both belong inside the seal; only `upload_id` and `chunk_index`
stay out, because the client matches on them.
+
+ `resume_from` answers the probe chunk (`UPLOAD_PROBE_INDEX`): how many
+ chunks of this file the node already holds. Inside the seal like the rest —
+ it is a fact about the operator's disk — and absent from an ordinary ack, so
+ a client can tell the two apart without looking at `chunk_index`.
"""
payload = {"filename": filename, "stored_as": stored_as, "dir": dir}
+ if resume_from is not None:
+ payload["resume_from"] = int(resume_from)
return {
"type": MNP.FILE_UPLOAD_ACK,
"v": MNP_VERSION,