diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ui/app.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/ui/app.py | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py index 28654df..b78f78d 100644 --- a/packages/meshbay-node/src/meshbay_node/ui/app.py +++ b/packages/meshbay-node/src/meshbay_node/ui/app.py @@ -23,6 +23,7 @@ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query from fastapi.responses import HTMLResponse, JSONResponse from meshbay_node import __version__ +from meshbay_node.config import DEFAULT_CONFIG_PATH from meshbay_common.crypto import generate_gek, wrap_gek_aes from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR @@ -127,6 +128,73 @@ def create_ui_app(state: dict) -> FastAPI: }) return {"groups": result} + @app.post("/api/groups/attach") + async def attach_group(payload: dict): + """ + Write a new [[groups]] block into node.toml. + + The name-to-id lookup happens here because this process is the one logged + into the hub. Nothing is created on the hub: the group already exists, + this only tells the node to host it. + """ + name = (payload.get("name") or "").strip() + shared_dir = (payload.get("shared_dir") or "").strip() + if not name or not shared_dir: + return JSONResponse({"error": "name and shared_dir are required"}, 400) + + config = state.get("config") + if not config: + return JSONResponse({"error": "No config loaded"}, 503) + + hub = state.get("hub") + if not hub or not hub._session: + return JSONResponse({"error": "Hub not connected"}, 503) + try: + mine = await hub.list_my_groups() + except Exception as e: + return JSONResponse({"error": f"Could not list groups: {e}"}, 502) + + match = [g for g in mine if g["id"] == name or g["name"] == name] + if not match: + return JSONResponse({ + "error": f"No group of yours is called {name!r}", + "available": [{"name": g["name"], "id": g["id"]} for g in mine], + }, 404) + if len(match) > 1: + return JSONResponse({ + "error": f"Several of your groups are called {name!r} — use the id", + "available": [{"name": g["name"], "id": g["id"]} for g in match], + }, 409) + group = match[0] + + if any(g.id == group["id"] for g in config.groups): + return JSONResponse( + {"error": f"{group['name']!r} is already hosted by this node"}, 409) + + path = Path(shared_dir).expanduser() + try: + path.mkdir(parents=True, exist_ok=True) + except OSError as e: + return JSONResponse({"error": f"Cannot create {path}: {e}"}, 400) + + # Appended as text rather than re-serialised: node.toml is hand-written + # and full of comments explaining decisions, and a round trip through a + # TOML writer would throw all of that away. + conf_path = Path(state.get("config_path") or DEFAULT_CONFIG_PATH) + block = (f'\n[[groups]]\n' + f'id = "{group["id"]}"\n' + f'name = "{group["name"]}"\n' + f'shared_dir = "{path}"\n' + f'visibility = "{group.get("visibility", "private")}"\n') + try: + with conf_path.open("a") as f: + f.write(block) + except OSError as e: + return JSONResponse({"error": f"Cannot write {conf_path}: {e}"}, 500) + + return {"group_id": group["id"], "name": group["name"], + "shared_dir": str(path), "config": str(conf_path)} + @app.get("/api/groups/{group_id}/files") async def api_group_files(group_id: str): groups_ctx = state.get("groups_ctx", {}) |