aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py71
1 files changed, 52 insertions, 19 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 92f2951..3bb0df7 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -646,9 +646,9 @@ class WebRTCPeerSession:
elif mtype == MNP.FILE_UPLOAD:
self._spawn(self._do_file_upload(msg))
elif mtype == MNP.DIR_CREATE:
- self._do_dir_create(msg)
+ self._spawn(self._do_dir_create(msg))
elif mtype == MNP.DIR_DELETE:
- self._do_dir_delete(msg)
+ self._spawn(self._do_dir_delete(msg))
elif mtype == MNP.FILE_DELETE:
self._do_file_delete(msg)
elif mtype == MNP.ADMIN_RESPONSE:
@@ -2034,7 +2034,7 @@ class WebRTCPeerSession:
self._send(reply)
self._audit_join("gek_wrapped", f"group={group_id[:8]}")
- def _do_dir_create(self, msg: dict) -> None:
+ async def _do_dir_create(self, msg: dict) -> None:
"""
Create a directory, for any member of the group.
@@ -2085,20 +2085,19 @@ class WebRTCPeerSession:
"code": "root_unavailable"})
return
- parent = safe_subdir(roots, parent_rel)
- if parent is None or not parent.is_dir():
+ parent = await off_disk(roots, safe_subdir, roots, parent_rel)
+ if parent is None or not await off_disk(roots, parent.is_dir):
self._send({"type": "error", "detail": "Invalid directory"})
return
- target = safe_subdir(roots, f"{parent_rel}/{name}")
+ target = await off_disk(roots, safe_subdir, roots, f"{parent_rel}/{name}")
if target is None:
self._send({"type": "error", "detail": "Invalid directory"})
return
- if target.exists():
- self._send({"type": "error", "detail": "Already exists"})
+ refusal = await off_disk(roots, _mkdir_if_absent, target)
+ if refusal is not None:
+ self._send({"type": "error", "detail": refusal})
return
-
- target.mkdir(parents=False)
virtual = roots.virtual_of(target) or f"{parent_rel}/{name}"
log.info("Directory created by %s: %s", self._user_id[:8], virtual)
self._audit("dir_create", virtual)
@@ -2113,7 +2112,7 @@ class WebRTCPeerSession:
found = roots.split(rel or "")
return found is not None and not found[1]
- def _do_dir_delete(self, msg: dict) -> None:
+ async def _do_dir_delete(self, msg: dict) -> None:
"""
Remove an empty directory, for the node operator.
@@ -2131,17 +2130,17 @@ class WebRTCPeerSession:
return
rel = (msg.get("dir") or "").strip("/")
- target = safe_subdir(roots, rel)
+ target = await off_disk(roots, safe_subdir, roots, rel)
# A root itself is not deletable here: removing one is a configuration
# change, and doing it through a file operation would leave the group
# config naming a directory nobody can reach.
if target is None or self._names_a_root(roots, rel):
self._send({"type": "error", "detail": "Invalid directory"})
return
- if not target.is_dir():
+ if not await off_disk(roots, target.is_dir):
self._send({"type": "error", "detail": "Not a directory"})
return
- if any(target.iterdir()):
+ if not await off_disk(roots, _is_empty_dir, target):
self._send({"type": "error", "detail": "Directory is not empty"})
return
if not self._has_admin_authority():
@@ -2157,9 +2156,9 @@ class WebRTCPeerSession:
rel = pending["subject"]
ctx = self._group_ctx()
roots: RootSet | None = ctx.get("roots")
- target = safe_subdir(roots, rel) if roots else None
+ target = await off_disk(roots, safe_subdir, roots, rel) if roots else None
if (target is None or self._names_a_root(roots, rel)
- or not target.is_dir()):
+ or not await off_disk(roots, target.is_dir)):
self._send({"type": "error", "detail": "Not a directory"})
return
@@ -2173,11 +2172,9 @@ class WebRTCPeerSession:
# Checked again after the signature: the emptiness test that let this
# through happened before a round trip to the operator's browser, and a
# file could have landed in the meantime.
- if any(target.iterdir()):
+ if not await off_disk(roots, _rmdir_if_empty, target):
self._send({"type": "error", "detail": "Directory is not empty"})
return
-
- target.rmdir()
log.info("Directory removed by %s: %s", self._user_id[:8], rel)
self._audit("dir_delete", rel)
self._send({"type": MNP.DIR_DELETE_ACK, "v": MNP_VERSION, "dir": rel})
@@ -6607,6 +6604,42 @@ class WebRTCPeerSession:
await self._pc.close()
+def _mkdir_if_absent(target: Path) -> str | None:
+ """
+ Create a directory unless it is already there, or say why not.
+
+ Both in one call, not a check awaited and then an act: the disk thread is
+ one worker, so nothing can slip between them. Split across two awaits, two
+ members creating the same name would both find nothing there and the second
+ `mkdir` would raise where a refusal was meant. Blocking; called through
+ `off_disk`.
+ """
+ if target.exists():
+ return "Already exists"
+ target.mkdir(parents=False)
+ return None
+
+
+def _is_empty_dir(target: Path) -> bool:
+ """Blocking; called through `off_disk`."""
+ return not any(target.iterdir())
+
+
+def _rmdir_if_empty(target: Path) -> bool:
+ """
+ Remove a directory if nothing is in it. False if something is.
+
+ The emptiness test and the removal are one call for the reason the caller
+ re-tests at all: the first test happened before a round trip to the
+ operator's browser, and a file can land in between. Two awaits here would
+ reopen the same window one size smaller. Blocking; called through `off_disk`.
+ """
+ if any(target.iterdir()):
+ return False
+ target.rmdir()
+ return True
+
+
def _locate(roots: RootSet, entry) -> tuple[Path | None, str | None]:
"""
Where an entry is, and whether it is readable — or the refusal to send.