A Python port of the Invisible Internet Project (I2P)
1"""SQLite-backed NetDB cache for persistent RouterInfo storage.
2
3Stores RouterInfo entries in ~/.i2p-python/netdb.sqlite3. Survives
4restarts so the router doesn't need to reseed every time. Uses WAL
5mode for concurrent read/write from maintenance tasks.
6
7Schema designed to support future reputation system columns.
8"""
9
10from __future__ import annotations
11
12import sqlite3
13import time
14from pathlib import Path
15
16
17class SqliteNetDB:
18 """SQLite-backed persistent NetDB cache."""
19
20 def __init__(self, data_dir: str) -> None:
21 db_path = Path(data_dir).expanduser() / "netdb.sqlite3"
22 db_path.parent.mkdir(parents=True, exist_ok=True)
23 self._conn = sqlite3.connect(str(db_path))
24 self._conn.execute("PRAGMA journal_mode=WAL")
25 self._conn.execute("""
26 CREATE TABLE IF NOT EXISTS router_info (
27 key BLOB PRIMARY KEY,
28 data BLOB NOT NULL,
29 received_at INTEGER NOT NULL,
30 expires_at INTEGER NOT NULL
31 )
32 """)
33 self._conn.commit()
34
35 def store(self, key: bytes, data: bytes, ttl_ms: int = 86_400_000) -> None:
36 """Store or replace a RouterInfo entry.
37
38 Parameters
39 ----------
40 key:
41 32-byte router identity hash.
42 data:
43 Raw RouterInfo bytes.
44 ttl_ms:
45 Time-to-live in milliseconds (default 24h).
46 """
47 now = int(time.time() * 1000)
48 self._conn.execute(
49 "INSERT OR REPLACE INTO router_info (key, data, received_at, expires_at) "
50 "VALUES (?, ?, ?, ?)",
51 (key, data, now, now + ttl_ms),
52 )
53 self._conn.commit()
54
55 def load_all(self) -> list[tuple[bytes, bytes]]:
56 """Load all non-expired entries.
57
58 Returns
59 -------
60 list[tuple[bytes, bytes]]
61 List of (key, data) tuples.
62 """
63 now = int(time.time() * 1000)
64 rows = self._conn.execute(
65 "SELECT key, data FROM router_info WHERE expires_at > ?", (now,)
66 ).fetchall()
67 return rows
68
69 def evict_expired(self) -> int:
70 """Delete expired entries.
71
72 Returns
73 -------
74 int
75 Number of entries deleted.
76 """
77 now = int(time.time() * 1000)
78 cur = self._conn.execute(
79 "DELETE FROM router_info WHERE expires_at <= ?", (now,)
80 )
81 self._conn.commit()
82 return cur.rowcount
83
84 def count(self) -> int:
85 """Return total entry count (including expired)."""
86 return self._conn.execute(
87 "SELECT COUNT(*) FROM router_info"
88 ).fetchone()[0]
89
90 def close(self) -> None:
91 """Close the database connection."""
92 self._conn.close()