- Add persistent geo_cache SQLite table (db.py) - Rewrite geo_service: batch API (100 IPs/call), two-tier cache, no caching of failed lookups so they are retried - Pre-warm geo cache from DB on startup (main.py lifespan) - Rewrite bans_by_country: SQL GROUP BY ip aggregation + lookup_batch instead of 2000-row fetch + asyncio.gather individual calls - Pre-warm geo cache after blocklist import (blocklist_service) - Add 300ms debounce to useMapData hook to cancel stale requests - Add perf benchmark asserting <2s for 10k bans - Add seed_10k_bans.py script for manual perf testing
113 lines
3.8 KiB
Python
113 lines
3.8 KiB
Python
"""Application database schema definition and initialisation.
|
|
|
|
BanGUI maintains its own SQLite database that stores configuration, session
|
|
state, blocklist source definitions, and import run logs. This module is
|
|
the single source of truth for the schema — all ``CREATE TABLE`` statements
|
|
live here and are applied on first run via :func:`init_db`.
|
|
|
|
The fail2ban database is separate and is accessed read-only by the history
|
|
and ban services.
|
|
"""
|
|
|
|
import aiosqlite
|
|
import structlog
|
|
|
|
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DDL statements
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_CREATE_SETTINGS: str = """
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key TEXT NOT NULL UNIQUE,
|
|
value TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
);
|
|
"""
|
|
|
|
_CREATE_SESSIONS: str = """
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
token TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
expires_at TEXT NOT NULL
|
|
);
|
|
"""
|
|
|
|
_CREATE_SESSIONS_TOKEN_INDEX: str = """
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions (token);
|
|
"""
|
|
|
|
_CREATE_BLOCKLIST_SOURCES: str = """
|
|
CREATE TABLE IF NOT EXISTS blocklist_sources (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
url TEXT NOT NULL UNIQUE,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
);
|
|
"""
|
|
|
|
_CREATE_IMPORT_LOG: str = """
|
|
CREATE TABLE IF NOT EXISTS import_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
source_id INTEGER REFERENCES blocklist_sources(id) ON DELETE SET NULL,
|
|
source_url TEXT NOT NULL,
|
|
timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
ips_imported INTEGER NOT NULL DEFAULT 0,
|
|
ips_skipped INTEGER NOT NULL DEFAULT 0,
|
|
errors TEXT
|
|
);
|
|
"""
|
|
|
|
_CREATE_GEO_CACHE: str = """
|
|
CREATE TABLE IF NOT EXISTS geo_cache (
|
|
ip TEXT PRIMARY KEY,
|
|
country_code TEXT,
|
|
country_name TEXT,
|
|
asn TEXT,
|
|
org TEXT,
|
|
cached_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
);
|
|
"""
|
|
|
|
# Ordered list of DDL statements to execute on initialisation.
|
|
_SCHEMA_STATEMENTS: list[str] = [
|
|
_CREATE_SETTINGS,
|
|
_CREATE_SESSIONS,
|
|
_CREATE_SESSIONS_TOKEN_INDEX,
|
|
_CREATE_BLOCKLIST_SOURCES,
|
|
_CREATE_IMPORT_LOG,
|
|
_CREATE_GEO_CACHE,
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def init_db(db: aiosqlite.Connection) -> None:
|
|
"""Create all BanGUI application tables if they do not already exist.
|
|
|
|
This function is idempotent — calling it on an already-initialised
|
|
database has no effect. It should be called once during application
|
|
startup inside the FastAPI lifespan handler.
|
|
|
|
Args:
|
|
db: An open :class:`aiosqlite.Connection` to the application database.
|
|
"""
|
|
log.info("initialising_database_schema")
|
|
async with db.execute("PRAGMA journal_mode=WAL;"):
|
|
pass
|
|
async with db.execute("PRAGMA foreign_keys=ON;"):
|
|
pass
|
|
for statement in _SCHEMA_STATEMENTS:
|
|
await db.executescript(statement)
|
|
await db.commit()
|
|
log.info("database_schema_ready")
|