diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/indexer.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/indexer/indexer.py | 57 |
1 files changed, 54 insertions, 3 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py index 33e7210..852c061 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py @@ -436,7 +436,7 @@ class DirectoryIndexer: cached = await self._cache.lookup( str(file_path), st.st_size, st.st_mtime, expected_hv) if cached is not None: - return IndexEntry( + return await self._attribute(IndexEntry( id=cached.hash, name=file_path.name, path=_virtual_dir(root, file_path), @@ -444,7 +444,7 @@ class DirectoryIndexer: type=cached.type, added_at=cached.added_at, hash_version=cached.hash_version, - ) + ), file_path, st) loop = asyncio.get_event_loop() entry = await loop.run_in_executor(self._executor, _scan_file, root, file_path) @@ -452,8 +452,51 @@ class DirectoryIndexer: await self._cache.put(str(file_path), st.st_size, st.st_mtime, entry.id, entry.type, entry.added_at, entry.hash_version) + return await self._attribute(entry, file_path, st) + + async def _attribute(self, entry: IndexEntry | None, file_path: Path, + st) -> IndexEntry | None: + """Stamp an entry with whoever sent the file, if a member did. + + Here, rather than beside each `add_entry`, because this is the one + funnel every entry passes through: the initial scan, the watchdog, + reconcile and a replug all build theirs from `_hash_or_cached`. + + The attribution used to be written at the end of the *upload* instead, + by walking the index for an entry that by construction did not exist + yet — the watchdog has not fired, and the `.part` the file was until the + rename is not indexable. It matched nothing, silently, so every uploaded + file was owned by nobody and `file_delete` refused everyone but the + operator, where MESHBAY_DESIGN.md §5.4 grants it to any non-revoked + device of the uploading account. + """ + if entry is None or self._cache is None: + return entry + who = await self._cache.uploader(str(file_path), st.st_size, st.st_mtime) + if who is not None: + entry.uploader_id, entry.uploader_pk = who return entry + async def record_upload(self, file_path: Path, user_id: str, + pk_ed25519: str) -> None: + """Remember who sent this file, for the entry that does not exist yet. + + Called by the transport once the last chunk has landed and the file is + at its final name. Durable rather than in-memory: the index is rebuilt + from disk at every start, and an owner the node forgets on restart is an + owner who cannot delete their own file tomorrow. + """ + if self._cache is None or not user_id: + return + try: + st = file_path.stat() + except OSError: + # Gone between the rename and here. Nothing to attribute, and + # nothing for anyone to delete either. + return + await self._cache.record_upload( + str(file_path), st.st_size, st.st_mtime, user_id, pk_ed25519 or "") + def _report_collisions(self) -> None: """ Names that are the same file on a case-insensitive filesystem. @@ -749,7 +792,15 @@ class DirectoryIndexer: if old is None: continue for field in self._ENRICHED_FIELDS: - setattr(entry, field, getattr(old, field)) + # What the rescan itself established wins. `uploader_id` and + # `uploader_pk` now come off the durable record (`_attribute`), + # and the entry being replaced is memory this process happens + # to still hold — so copying over them would let a stale blank + # beat the thing that survives a restart. Every other field is + # None on a freshly scanned entry, so for those this is exactly + # the carry-over it has always been. + if getattr(entry, field) is None: + setattr(entry, field, getattr(old, field)) # It came back intact, so it is not one of the entries the daemon # needs to enrich again. self.rescanned_ids.discard(entry.id) |