1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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 [],
}
|