aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/uploads.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/uploads.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/uploads.py33
1 files changed, 32 insertions, 1 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/uploads.py b/packages/meshbay-node/src/meshbay_node/uploads.py
index dd31e1e..c485e39 100644
--- a/packages/meshbay-node/src/meshbay_node/uploads.py
+++ b/packages/meshbay-node/src/meshbay_node/uploads.py
@@ -24,6 +24,8 @@ build first.
from __future__ import annotations
+import re
+import secrets
import time
from collections.abc import Iterable
from dataclasses import dataclass, field
@@ -34,6 +36,26 @@ from pathlib import Path
# recognise one, and a second spelling of it would be a bug nobody could see.
PART_SUFFIX = ".part"
+
+def part_name(stored_name: str) -> str:
+ """The `.part` one upload writes: its final name, a tag of its own, `.part`.
+
+ Its own, because the final name alone is shared: two uploads that settled
+ on one name — two groups hosting one folder, each with its own lock —
+ would write one file, the second truncating the first.
+ """
+ return f"{stored_name}.{secrets.token_hex(4)}{PART_SUFFIX}"
+
+
+# What `part_name` writes, and the only thing the reaper deletes. A `.part`
+# without the node's tag is somebody else's — a browser's download in progress in
+# a shared folder, a copy the operator is making — and is never touched.
+_OWN_PART = re.compile(r"\.[0-9a-f]{8}" + re.escape(PART_SUFFIX) + r"$")
+
+
+def is_own_part(path: Path) -> bool:
+ return bool(_OWN_PART.search(path.name))
+
# How long a `.part` with no upload behind it is kept before it is deleted.
#
# Generous on purpose. The cost of waiting is disk; the cost of being wrong is
@@ -107,6 +129,15 @@ class PartialUploads:
def drop(self, user_id: str, rel_dir: str, filename: str) -> Partial | None:
return self._by_key.pop((user_id, rel_dir, filename), None)
+ def reserved_names(self, rel_dir: str) -> set[str]:
+ """The final names uploads in flight into `rel_dir` will take.
+
+ None of them exists on disk yet, so a name check that looked only at
+ the directory would hand the same name to a second upload.
+ """
+ return {state.stored_name for (_u, d, _f), state in self._by_key.items()
+ if d == rel_dir}
+
def __len__(self) -> int:
return len(self._by_key)
@@ -145,7 +176,7 @@ def orphaned_parts(candidates: Iterable[tuple[Path, float]],
"""
doomed: list[Path] = []
for path, mtime in candidates:
- if path.suffix != PART_SUFFIX:
+ if not is_own_part(path):
continue
if path in live:
continue