diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/wire.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/wire.py | 76 |
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 [], + } |