aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/indexer/cache.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/indexer/cache.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/cache.py68
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: