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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
|
"""What the node hosts: groups, their directories (roots) and their keys."""
import sys
from pathlib import Path
from meshbay_node.cli.api import _daemon_api, _resolve_group
from meshbay_node.config import DEFAULT_CONFIG_PATH, load_config
def group(args) -> None:
if args.subcommand in (None, "list"):
# Milestone 14.2.
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
out = _daemon_api(cfg, "/api/groups")
groups = out.get("groups", [])
if not groups:
print("no groups hosted — add one with: "
"meshbay-node group add <name> --dir <path>")
return
for g in groups:
key = "GEK" if g.get("has_gek") else "NO KEY"
print(f" {g['name']} [{g['visibility']}/{g.get('join_policy')}] "
f"{key} {g['file_count']} file(s) "
f"{g.get('peers', 0)} peer(s)")
print(f" {g['id']}")
for r in g.get("roots", []):
flags = []
if r.get("writable"):
flags.append("rw")
else:
flags.append("ro")
if r.get("removable"):
flags.append("removable")
if r.get("ejected"):
flags.append("ejected")
flag_str = f" ({', '.join(flags)})" if flags else ""
live = "" if r.get("available", True) else " [UNAVAILABLE]"
print(f" root {r['name']}{flag_str}{live}")
if not g.get("has_gek"):
print(f" give it a key: meshbay-node gek init "
f"--group {g['name']}")
return
if args.subcommand == "remove":
if not args.target:
print("usage: meshbay-node group remove <name>")
sys.exit(1)
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
if not args.yes:
answer = input(f"Remove group '{args.target}' from this node? [y/N] ")
if answer.lower() not in ("y", "yes"):
print("cancelled")
return
out = _daemon_api(cfg, "/api/groups/detach", method="POST",
body={"name": args.target})
print(f"{out['name']} ({out['group_id'][:8]}) removed from {out['config']}")
print()
print("Restart the daemon to stop hosting it:")
print(" meshbay-node restart-daemon")
return
if args.subcommand != "add":
print("usage: meshbay-node group list|add|remove <name>")
sys.exit(1)
if not args.target or not args.dir:
print("usage: meshbay-node group add <name> --dir <path> "
"[--no-writable]")
print()
print("The group must already exist on the hub and be yours. This")
print("only tells the node to host it, and picks its first")
print("directory, which accepts uploads unless --no-writable.")
print("Add more with: meshbay-node root add <path> [--writable]")
sys.exit(1)
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
# Writable unless the operator says otherwise: a brand-new group that
# cannot receive a single file until its owner finds a second command
# is not a working group. Every root added *later* is read-only by
# default, which is the opposite rule and the right one there.
writable = args.writable is not False
body = {"name": args.target, "shared_dir": args.dir,
"writable": writable}
out = _daemon_api(cfg, "/api/groups/attach", method="POST", body=body)
print(f"{out['name']} ({out['group_id'][:8]}) added to {out['config']}")
print(f" shared_dir {out['shared_dir']}"
f" ({'read-write' if writable else 'read-only'})")
print()
print("Tell the daemon to re-read its config, then give the group a key:")
print(" meshbay-node reload")
print(f" meshbay-node gek init --group {out['name']}")
print()
print("The key is this group's own — members of your other groups cannot")
print("read it, and joining one says nothing about the other.")
return
def root(args) -> None:
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
sub = args.subcommand or "list"
group_id = _resolve_group(cfg, args.group)
if sub == "list":
out = _daemon_api(cfg, "/api/groups")
group = next((g for g in out.get("groups", [])
if g["id"] == group_id), None)
if not group:
print(f"group {group_id[:8]} not hosted on this node")
sys.exit(1)
roots = group.get("roots", [])
if not roots:
print("no roots configured")
print(f"add one: meshbay-node root add /path/to/dir --group {group_id}")
return
for r in roots:
flags = []
if r.get("writable"):
flags.append("rw")
else:
flags.append("ro")
if r.get("removable"):
flags.append("removable")
if r.get("ejected"):
flags.append("EJECTED")
avail = "available" if r.get("available", True) else "UNAVAILABLE"
flags.append(avail)
print(f" {r['name']:<20} {', '.join(flags)}")
print(f" {r.get('path', '?')}")
return
if sub == "add":
path = args.target
if not path:
print("usage: meshbay-node root add <path> [--name NAME] "
"[--writable] [--removable] [--group NAME]")
sys.exit(1)
body = {
"path": path,
"name": args.name or Path(path).name,
"writable": args.writable if args.writable is not None else True,
"removable": bool(args.removable),
}
_daemon_api(cfg, f"/api/groups/{group_id}/roots",
method="POST", body=body)
w = "rw" if body["writable"] else "ro"
rm = ", removable" if body["removable"] else ""
print(f"added root {body['name']} → {path} ({w}{rm})")
print("reload the daemon to start indexing:")
print(" meshbay-node reload")
return
if sub == "remove":
name = args.target
if not name:
print("usage: meshbay-node root remove <name> [--group NAME]")
sys.exit(1)
if not args.yes:
print(f"Remove root '{name}' from group {group_id[:8]}?")
print("Files on disk are untouched; only the node config changes.")
if input("remove? [y/N] ").strip().lower() not in ("y", "yes"):
print("cancelled")
return
_daemon_api(cfg, f"/api/groups/{group_id}/roots/{name}",
method="DELETE")
print(f"removed root {name}")
print("reload the daemon to apply:")
print(" meshbay-node reload")
return
if sub == "set":
name = args.target
if not name:
print("usage: meshbay-node root set <name> "
"[--writable|--no-writable] "
"[--removable|--no-removable] [--group NAME]")
sys.exit(1)
body = {}
if args.writable is not None:
body["writable"] = args.writable
if args.removable is not None:
body["removable"] = args.removable
if not body:
print("nothing to change — pass --writable/--no-writable "
"or --removable/--no-removable")
sys.exit(1)
_daemon_api(cfg, f"/api/groups/{group_id}/roots/{name}",
method="PATCH", body=body)
changes = ", ".join(f"{k}={v}" for k, v in body.items())
print(f"updated root {name}: {changes}")
return
if sub == "eject":
name = args.target
if not name:
print("usage: meshbay-node root eject <name> [--group NAME]")
sys.exit(1)
_daemon_api(cfg, f"/api/groups/{group_id}/roots/{name}/eject",
method="PUT")
print(f"ejected root {name} — files are hidden until plugged back")
return
if sub == "plug":
name = args.target
if not name:
print("usage: meshbay-node root plug <name> [--group NAME]")
sys.exit(1)
_daemon_api(cfg, f"/api/groups/{group_id}/roots/{name}/plug",
method="PUT")
print(f"plugged root {name} — files are visible again")
return
print("usage: meshbay-node root list|add|remove|set|eject|plug [name] "
"[--group NAME]")
sys.exit(1)
def gek(args) -> None:
# `gek-init` is the original spelling and still works. `gek rotate` is
# the one that matters after a revocation: the ex-member holds the
# current key and nothing else takes it from them.
sub = "init" if args.command == "gek-init" else (args.subcommand or "init")
if sub not in ("init", "rotate"):
print("usage: meshbay-node gek init|rotate [--group NAME]")
sys.exit(1)
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
group_id = _resolve_group(cfg, args.group)
if sub == "rotate" and not args.yes:
print("Rotating replaces this group's key.")
print(" · every member re-receives it automatically on their next connect")
print(" · anyone revoked keeps the OLD key and loses access to new content")
print(" · content already downloaded stays readable to whoever has it")
if input("rotate now? [y/N] ").strip().lower() not in ("y", "yes"):
print("cancelled")
return
out = _daemon_api(cfg, f"/api/groups/{group_id}/gek"
f"{'?rotate=true' if sub == 'rotate' else ''}",
method="POST", timeout=60)
verb = "rotated" if out.get("rotated") else "ready"
print(f"GEK {verb} for {group_id}")
print(f" {out.get('authorized_members', 0)} authorized member(s) — each "
f"receives the key on connect")
for err in out.get("errors") or []:
print(f" ! {err}")
return
|