aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/wire.py
blob: 4e09167adaf163a68f424b426bcf3b7ca833c9e3 (plain) (blame)
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 [],
    }