diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/cache.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/indexer/cache.py | 68 |
1 files changed, 68 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/cache.py b/packages/meshbay-node/src/meshbay_node/indexer/cache.py index c167db9..6c87f40 100644 --- a/packages/meshbay-node/src/meshbay_node/indexer/cache.py +++ b/packages/meshbay-node/src/meshbay_node/indexer/cache.py @@ -23,6 +23,7 @@ only daemon.py's wiring changed. """ import logging +import time from dataclasses import dataclass from pathlib import Path @@ -40,6 +41,25 @@ CREATE TABLE IF NOT EXISTS files ( added_at INTEGER NOT NULL, hash_version INTEGER NOT NULL DEFAULT 1 ); + +-- Who sent a file. Written when an upload finishes, read when the file is +-- indexed — two different moments, and the second is much later: the watchdog +-- debounces for two seconds and then hashes, so the entry does not exist yet +-- when the last chunk lands. Recording it in memory would also lose it at every +-- restart, where the index is rebuilt from disk, and an owner the node forgets +-- is an owner who cannot delete their own file tomorrow. +-- +-- Validated against a live stat() exactly as `files` is: a row whose size or +-- mtime no longer match is a different file at that path, and attributes +-- nothing. That is what makes a row left behind by a deleted file harmless. +CREATE TABLE IF NOT EXISTS uploads ( + path TEXT PRIMARY KEY, + size INTEGER NOT NULL, + mtime REAL NOT NULL, + user_id TEXT NOT NULL, + pk_ed25519 TEXT NOT NULL DEFAULT '', + at INTEGER NOT NULL +); """ _MIGRATE_V2 = "ALTER TABLE files ADD COLUMN hash_version INTEGER NOT NULL DEFAULT 1" @@ -59,6 +79,11 @@ class IndexCache: def __init__(self, db_path: Path): self._db_path = db_path self._db: aiosqlite.Connection | None = None + # Whether `uploads` holds anything at all. Every entry the indexer + # builds asks this cache who sent the file, and on a node that has + # never received an upload — most of them, most of the time — that is + # one query per file per scan for an answer that is always None. + self._has_uploads = False async def open(self) -> None: self._db_path.parent.mkdir(parents=True, exist_ok=True) @@ -69,6 +94,10 @@ class IndexCache: except Exception: pass # column already exists await self._db.commit() + async with self._db.execute( + "SELECT EXISTS(SELECT 1 FROM uploads)") as cur: + row = await cur.fetchone() + self._has_uploads = bool(row and row[0]) async def close(self) -> None: if self._db: @@ -114,6 +143,45 @@ class IndexCache: (path, mtime, size, hash, type, added_at, hash_version)) await self._db.commit() + # ── Who sent a file ────────────────────────────────────────────────────── + + async def record_upload(self, path: str, size: int, mtime: float, + user_id: str, pk_ed25519: str) -> None: + """Remember that this account put this file here. + + Written once the upload is complete and the file is at its final name, + never partway through — a `.part` is not indexable and would key a row + to a path that is about to change. + """ + await self._db.execute( + "INSERT INTO uploads (path, size, mtime, user_id, pk_ed25519, at) " + "VALUES (?, ?, ?, ?, ?, ?) " + "ON CONFLICT(path) DO UPDATE SET " + "size = excluded.size, mtime = excluded.mtime, " + "user_id = excluded.user_id, pk_ed25519 = excluded.pk_ed25519, " + "at = excluded.at", + (path, size, mtime, user_id, pk_ed25519, int(time.time()))) + await self._db.commit() + self._has_uploads = True + + async def uploader(self, path: str, size: int, + mtime: float) -> tuple[str, str] | None: + """Who sent the file currently at this path, or None. + + The size and mtime are matched exactly, like `lookup`: the path alone + would credit whoever last uploaded *a* file of that name for whatever + occupies the name now — including something the operator put there + themselves afterwards, which would hand a member the right to delete it. + """ + if not self._has_uploads: + return None + async with self._db.execute( + "SELECT user_id, pk_ed25519 FROM uploads " + "WHERE path = ? AND size = ? AND mtime = ?", + (path, size, mtime)) as cur: + row = await cur.fetchone() + return (row[0], row[1]) if row else None + # ── Maintenance (node admin UI "prune index cache") ────────────────────── async def count(self) -> int: |