diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-01 09:57:11 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-01 09:57:11 +0200 |
| commit | b3c031881b38b4c95e2945c05537b6a681a096d9 (patch) | |
| tree | 54f2a6975d3b7f6d760c8bf5775023d31b21c003 /packages/meshbay-node/src/meshbay_node/uploads.py | |
| parent | 0673922e704f718037eeffd2724debcfa8ac0b4b (diff) | |
| download | meshbay-b3c031881b38b4c95e2945c05537b6a681a096d9.tar.gz | |
fix(node): an upload never replaces a file, nor shares a part with another
A name an upload in flight will take is reserved; each upload writes its own
`name.<tag>.part`; the finished file is published by a hard link, which
refuses an existing target, and takes the next free name if one appeared
meanwhile — the last ack names it. Two members sending one name at once wrote
one part and published it twice; a file copied in during an upload was
replaced (F-09).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/uploads.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/uploads.py | 20 |
1 files changed, 20 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/uploads.py b/packages/meshbay-node/src/meshbay_node/uploads.py index dd31e1e..92b854e 100644 --- a/packages/meshbay-node/src/meshbay_node/uploads.py +++ b/packages/meshbay-node/src/meshbay_node/uploads.py @@ -24,6 +24,7 @@ build first. from __future__ import annotations +import secrets import time from collections.abc import Iterable from dataclasses import dataclass, field @@ -34,6 +35,16 @@ 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}" + # 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 +118,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) |