aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/wire.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/wire.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/wire.py76
1 files changed, 76 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/wire.py b/packages/meshbay-node/src/meshbay_node/transport/wire.py
new file mode 100644
index 0000000..4e09167
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/transport/wire.py
@@ -0,0 +1,76 @@
+"""
+Wire shapes shared by every node transport.
+
+`file_chunk` lives in `meshbay_common.protocol` — it is pure crypto and shape, so a
+client can use the same encoder. This module is for the messages that also need the
+node's own view of its disk, which `meshbay-common` cannot see.
+
+Why it exists at all: `index_sync` was built twice, and the two copies did not agree.
+WebRTC sent `{group_id, version, entries, dirs, roots}` — the shape the shipping
+client reads — while QUIC sent `{index_b64}`, a signed, compressed, GEK-encrypted
+envelope produced by `GroupIndex.serialize()`. Same message type, two encodings, one
+consumer each and nothing asserting they matched. Same failure mode as the two
+`file_chunk` encoders, and the same fix: one builder, used by both.
+
+`GroupIndex.serialize()`/`deserialize()` are unchanged and still tested — they remain
+a correct signed index envelope — but they no longer describe any MNP message. Read
+them as an at-rest/interchange format, not as a wire contract.
+"""
+
+from __future__ import annotations
+
+from meshbay_common import MNP_VERSION
+from meshbay_common.protocol import MNP, index_entry_wire
+
+from meshbay_node.roots import RootSet
+
+# A group with a deep tree can hold more directories than anyone will navigate in one
+# sitting, and the whole list rides on one message.
+MAX_DIRS = 2000
+
+
+def list_dirs(roots: RootSet | None) -> list[str]:
+ """
+ Every directory in the group, as members address them, sorted.
+
+ Each root appears as a directory in its own right, so a root holding no files yet
+ is still somewhere a member can navigate to and upload into. An unavailable root
+ is listed too — its content is frozen, not gone, and hiding it would look exactly
+ like deletion.
+ """
+ if not roots:
+ return []
+ out: list[str] = []
+ for root in roots:
+ out.append(root.name)
+ if not root.available:
+ continue
+ try:
+ for path in sorted(root.path.rglob("*")):
+ if path.is_dir() and not path.name.startswith("."):
+ rel = path.relative_to(root.path)
+ if not any(part.startswith(".") for part in rel.parts):
+ out.append(f"{root.name}/{rel.as_posix()}")
+ except OSError:
+ continue
+ return sorted(out)[:MAX_DIRS]
+
+
+def index_sync_message(index, roots: RootSet | None) -> dict:
+ """
+ The full `index_sync` message for one group.
+
+ `dirs` and `roots` are here because directories are not index entries: without
+ them a folder someone just created, or one they emptied, does not exist as far as
+ a client is concerned, and a member cannot tell "the drive is unplugged" from "it
+ is all still there".
+ """
+ return {
+ "type": MNP.INDEX_SYNC,
+ "v": MNP_VERSION,
+ "group_id": index.group_id,
+ "version": index.version,
+ "entries": [index_entry_wire(e) for e in index.entries],
+ "dirs": list_dirs(roots),
+ "roots": roots.describe() if roots else [],
+ }