"""SQLite-backed NetDB cache for persistent RouterInfo storage. Stores RouterInfo entries in ~/.i2p-python/netdb.sqlite3. Survives restarts so the router doesn't need to reseed every time. Uses WAL mode for concurrent read/write from maintenance tasks. Schema designed to support future reputation system columns. """ from __future__ import annotations import sqlite3 import time from pathlib import Path class SqliteNetDB: """SQLite-backed persistent NetDB cache.""" def __init__(self, data_dir: str) -> None: db_path = Path(data_dir).expanduser() / "netdb.sqlite3" db_path.parent.mkdir(parents=True, exist_ok=True) self._conn = sqlite3.connect(str(db_path)) self._conn.execute("PRAGMA journal_mode=WAL") self._conn.execute(""" CREATE TABLE IF NOT EXISTS router_info ( key BLOB PRIMARY KEY, data BLOB NOT NULL, received_at INTEGER NOT NULL, expires_at INTEGER NOT NULL ) """) self._conn.commit() def store(self, key: bytes, data: bytes, ttl_ms: int = 86_400_000) -> None: """Store or replace a RouterInfo entry. Parameters ---------- key: 32-byte router identity hash. data: Raw RouterInfo bytes. ttl_ms: Time-to-live in milliseconds (default 24h). """ now = int(time.time() * 1000) self._conn.execute( "INSERT OR REPLACE INTO router_info (key, data, received_at, expires_at) " "VALUES (?, ?, ?, ?)", (key, data, now, now + ttl_ms), ) self._conn.commit() def load_all(self) -> list[tuple[bytes, bytes]]: """Load all non-expired entries. Returns ------- list[tuple[bytes, bytes]] List of (key, data) tuples. """ now = int(time.time() * 1000) rows = self._conn.execute( "SELECT key, data FROM router_info WHERE expires_at > ?", (now,) ).fetchall() return rows def evict_expired(self) -> int: """Delete expired entries. Returns ------- int Number of entries deleted. """ now = int(time.time() * 1000) cur = self._conn.execute( "DELETE FROM router_info WHERE expires_at <= ?", (now,) ) self._conn.commit() return cur.rowcount def count(self) -> int: """Return total entry count (including expired).""" return self._conn.execute( "SELECT COUNT(*) FROM router_info" ).fetchone()[0] def close(self) -> None: """Close the database connection.""" self._conn.close()