diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-11 14:55:52 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-11 14:55:52 +0200 |
| commit | cc90dc943fcd0e7bbf52674fb3f95ff097026f4a (patch) | |
| tree | 6315a5e966c027c50b92d59e60c4b506a418143e /packages/meshbay-node/src/meshbay_node | |
| parent | edde9e441fb6b84e9d56215d6e2a8d9338b8f962 (diff) | |
| download | meshbay-cc90dc943fcd0e7bbf52674fb3f95ff097026f4a.tar.gz | |
feat: Phase 10b — Self-service UI (group create/join, upload, IndexedDB, search)
Six self-service features for the web SPA:
- Group creation UI with GEK auto-generation (AES-256-GCM ECIES)
- Member management + invite by username (GEK wrapping for invitee)
- Open group self-join flow (POST /v1/groups/{id}/join)
- File upload client→node (FILE_UPLOAD MNP type, .uploads/ staging)
- IndexedDB caching of group file indexes (instant display on revisit)
- Cross-group file search (SearchPage, pure client-side on cached indexes)
11 new tests (166 total): 8 group self-service + 3 AES GEK wrap/unwrap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 44 |
1 files changed, 44 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 7da6623..94d9b04 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -114,6 +114,8 @@ class WebRTCPeerSession: self._do_chat_message(msg) elif mtype == MNP.CHAT_HISTORY: self._do_chat_history(msg) + elif mtype == MNP.FILE_UPLOAD: + self._do_file_upload(msg) else: log.warning("Unknown MNP message type on DataChannel: %s", mtype) except Exception as e: @@ -323,6 +325,48 @@ class WebRTCPeerSession: ], }) + def _do_file_upload(self, msg: dict) -> None: + ctx = self._group_ctx() + filename = msg.get("filename", "") + chunk_index = msg.get("chunk_index", 0) + total_chunks = msg.get("total_chunks", 1) + data = msg.get("data") + + if not filename or data is None: + self._send({"type": "error", "detail": "Missing filename or data"}) + return + + shared_root = ctx.get("shared_root") + if not shared_root: + self._send({"type": "error", "detail": "No shared directory"}) + return + + upload_dir = shared_root / ".uploads" + upload_dir.mkdir(exist_ok=True) + safe_name = filename.replace("/", "_").replace("\\", "_").replace("..", "_") + tmp_path = upload_dir / f"{safe_name}.part" + + if isinstance(data, str): + chunk_bytes = base64.b64decode(data) + else: + chunk_bytes = bytes(data) + + mode = "ab" if chunk_index > 0 else "wb" + with open(tmp_path, mode) as f: + f.write(chunk_bytes) + + self._send({ + "type": MNP.FILE_UPLOAD_ACK, + "v": MNP_VERSION, + "chunk_index": chunk_index, + "filename": filename, + }) + + if chunk_index + 1 >= total_chunks: + final_path = shared_root / safe_name + tmp_path.rename(final_path) + log.info("Upload complete: %s (%d chunks)", safe_name, total_chunks) + def _send(self, obj: dict) -> None: if self._channel and self._channel.readyState == "open": self._channel.send(_pack(obj)) |