aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 12:01:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 12:01:06 +0200
commit4f5d3d4ac151874f03c6fcc451d6b1d5bb1efb78 (patch)
tree4de7dbb57b7e4342c0ee41f26268aab90117f7df /packages/meshbay-common/src
parentd62b6a4e8985d0504e1825f6f8f663ccd64489ae (diff)
downloadmeshbay-4f5d3d4ac151874f03c6fcc451d6b1d5bb1efb78.tar.gz
feat: resume an interrupted upload, and pause one
Stage 8 of ~/next/improve-downloads.md, second half, plus the gap it exposed in stage 7. **Asking where to resume.** 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 this path bought in MNP 2.0: before it, 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 an ordinary `file_upload` with no bytes and `chunk_index: -1`. The node writes nothing, creates no state, reserves no name, and answers with `resume_from` in the sealed ack. A node that predates it refuses the index, which the client reads as "start from the beginning" — the behaviour it had anyway — and the wait is bounded so one that answers neither does not strand an upload. The probe is answered after every check the write path makes, so it cannot ask questions about a directory the caller may not write to, and it answers only about the member who asks: otherwise one member could measure another's progress on a file they never sent, and worse, resume it. **Pausing an upload.** Reported: no pause button on an upload, even in the desktop app. Stage 7 built pause around the download path — a target declares whether it can be stopped — and an upload has no local target to ask. It was also refused by design, since a transfer handed a lease it cannot re-create must not be offered a button that would drop its slot for good. Uploads now ask for their slot rather than being handed one, and say they are pausable outright: a File is seekable and the node keeps the position. Resuming re-probes rather than trusting the client's own memory, so it works across a reconnect too. **And the slot they hold.** `_do_file_upload` never called `slots.touch(tr)`. Chunks are not gated by the lease, so the file arrived — but the node reclaimed a grant nobody appeared to be using after thirty seconds, twice, then abandoned it, and the widget follows the lease. Measured from the journal: a 3.5 GB upload read "waiting, 0 ahead" for a minute and a half while it was transferring. The download twin of this was fixed on 2026-09-08; the same omission was still here, invisible until uploads took a real lease. `test_the_upload_itself_is_sealed` now checks every message `uploadFile` sends rather than the first. Adding the probe put a second one in front of the one it was written for, and it would have kept passing while guarding nothing. Node suite 1202 passed, hub suite 850 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-common/src')
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py24
1 files changed, 24 insertions, 0 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
index 8a521bb..5bd2903 100644
--- a/packages/meshbay-common/src/meshbay_common/protocol.py
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -501,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,
@@ -510,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.
@@ -518,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,