aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/daemon.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-06 16:05:39 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-06 16:05:39 +0200
commite76e27868b30a2b00b1ba42dd8e7ee6071e0c0d7 (patch)
tree3c23483207d77adf3b67f290b67a57ce185fb1a1 /packages/meshbay-node/src/meshbay_node/daemon.py
parent0ed078c92cabab1dab0f70f321562032ea549ce6 (diff)
downloadmeshbay-e76e27868b30a2b00b1ba42dd8e7ee6071e0c0d7.tar.gz
feat: groups refactor Phase 1 — root RO/RW model + shared directories UI
Replace the upload boolean with per-root writable/removable/ejected flags. Backend: new ops (update_root, eject_root, plug_root), MNP 1.1 protocol messages, live RootSet updates so API always reflects current state, CLI root subcommand (add/remove/set/list/eject/plug). Frontend: SharedDirectoriesTable with optimistic toggle switches, eject/plug in Files and Settings, upload gated on root.writable, ejected-root filtering in all media apps, updated Create Group wizard, 10-locale i18n. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/daemon.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py185
1 files changed, 172 insertions, 13 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index ea13680..f45101f 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -1663,15 +1663,17 @@ def main() -> None:
parser = argparse.ArgumentParser(description="MeshBay Node daemon")
parser.add_argument("command", nargs="?",
choices=["init", "reset", "status", "gek-init",
- "gek", "operator", "member", "group", "file",
- "video", "denylist", "stun", "reload",
+ "gek", "operator", "member", "group", "root",
+ "file", "video", "denylist", "stun", "reload",
"restart-daemon", "autostart", "service",
"calibrate-argon2"],
help="init: provision config + keystore | reset: erase all "
"node state | status: node state and keys "
"| operator pair: pair a "
"browser with this node | member list|invite|revoke|unpin "
- "| group list|add|remove | gek init|rotate | file list|rm "
+ "| group list|add|remove "
+ "| root list|add|remove|set|eject|plug "
+ "| gek init|rotate | file list|rm "
"| video rematch: re-resolve TMDB matches for a group's "
"videos | denylist show|clear "
"| stun list|add|remove|reset "
@@ -1687,7 +1689,9 @@ def main() -> None:
"| calibrate-argon2: benchmark")
parser.add_argument("subcommand", nargs="?",
help="'pair' for operator; list|invite|revoke|unpin for "
- "member; list|add|remove for group; init|rotate for gek; "
+ "member; list|add|remove for group; "
+ "list|add|remove|set|eject|plug for root; "
+ "init|rotate for gek; "
"list|rm for file; rematch for video; show|clear for "
"denylist; list|add|remove|reset for stun; "
"install|remove|start|stop|status for autostart and "
@@ -1710,14 +1714,29 @@ def main() -> None:
help="Config file path")
parser.add_argument("--group", default=None,
help="group id (optional if only one is configured)")
+ parser.add_argument("--writable", action="store_true", default=None,
+ dest="writable",
+ help="mark root as read-write (root set/add)")
+ parser.add_argument("--no-writable", action="store_false",
+ dest="writable",
+ help="mark root as read-only (root set)")
+ parser.add_argument("--removable", action="store_true", default=None,
+ dest="removable",
+ help="mark root as removable (root set/add)")
+ parser.add_argument("--no-removable", action="store_false",
+ dest="removable",
+ help="mark root as not removable (root set)")
+ parser.add_argument("--name", default=None,
+ help="root name (root add; defaults to directory basename)")
parser.add_argument("--log-level", default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR"])
args = parser.parse_args()
# Query commands print a report; library logging would interleave with it.
quiet = args.command in ("status", "gek-init", "gek", "operator",
- "member", "group", "file", "video", "denylist",
- "stun", "reload", "restart-daemon", "reset")
+ "member", "group", "root", "file", "video",
+ "denylist", "stun", "reload", "restart-daemon",
+ "reset")
logging.basicConfig(
level=logging.ERROR if quiet else getattr(logging, args.log_level),
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
@@ -1961,9 +1980,16 @@ def main() -> None:
print(" <no directory configured>")
for r in g.roots:
label = r.name or Path(r.path).name
- flag = " (uploads)" if r.upload else ""
+ flags = []
+ if getattr(r, 'writable', False) or getattr(r, 'upload', False):
+ flags.append("rw")
+ else:
+ flags.append("ro")
+ if getattr(r, 'removable', False):
+ flags.append("removable")
+ flag_str = f" ({', '.join(flags)})" if flags else ""
live = "" if Path(r.path).expanduser().is_dir() else " [UNAVAILABLE]"
- print(f" {label} → {r.path}{flag}{live}")
+ print(f" {label} → {r.path}{flag_str}{live}")
# Node authority: the roster is the source of truth, node.toml the legacy
# form. Read the DB directly so this reports correctly while the daemon is
# stopped — the state an operator is most often in when checking.
@@ -2408,11 +2434,18 @@ def main() -> None:
f"{g.get('peers', 0)} peer(s)")
print(f" {g['id']}")
for r in g.get("roots", []):
- flags = ""
- if r.get("upload"):
- flags = " (uploads, direct)" if r.get("direct") else " (uploads)"
+ 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']}{flags}{live}")
+ 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']}")
@@ -2450,10 +2483,18 @@ def main() -> None:
cfg = load_config(args.config or DEFAULT_CONFIG_PATH)
body = {"name": args.target, "shared_dir": args.dir}
if args.upload_dir:
+ import warnings
+ warnings.warn(
+ "--upload-dir is deprecated; the main root is writable by "
+ "default. Use 'meshbay-node root add' for additional roots.",
+ DeprecationWarning, stacklevel=1)
+ print("WARNING: --upload-dir is deprecated. The shared directory is "
+ "writable by default. Use 'meshbay-node root add' for "
+ "additional roots.")
body["upload_dir"] = args.upload_dir
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']}")
+ print(f" shared_dir {out['shared_dir']} (writable)")
if out.get("upload_dir"):
print(f" upload_dir {out['upload_dir']}")
print()
@@ -2465,6 +2506,124 @@ def main() -> None:
print("read it, and joining one says nothing about the other.")
return
+ if args.command == "root":
+ 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, f"/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)
+
if args.command == "operator":
if args.subcommand != "pair":
print("usage: meshbay-node operator pair")