radio: add /api/db.sqlite for offline laptop sync

Streams the read-only archive.db over the same Tailscale-routed port
as the search service. Companion to azcomputerguru/radio-archive-portable
which curl-fetches from this endpoint and runs locally on the laptop.

Disclosure equivalent to /api/search (which already exposes every
transcript), so no auth added. Deployed to Jupiter; verified GET
returns 60 MB SQLite blob with all 1,405 classifier rows intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 19:50:50 -07:00
parent 8d4bb16255
commit 5e3b1a2297

View File

@@ -20,7 +20,7 @@ from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, HTTPException, Query from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import HTMLResponse from fastapi.responses import FileResponse, HTMLResponse
DB_PATH = os.environ.get("ARCHIVE_DB", "/data/archive.db") DB_PATH = os.environ.get("ARCHIVE_DB", "/data/archive.db")
PORT = int(os.environ.get("PORT", "8765")) PORT = int(os.environ.get("PORT", "8765"))
@@ -218,6 +218,23 @@ def stats():
return {"counts": counts, "by_year": by_year} return {"counts": counts, "by_year": by_year}
@app.get("/api/db.sqlite")
def download_db():
"""Stream the read-only archive.db for offline laptop sync.
Anyone who can reach /api/search can already read every transcript,
so exposing the underlying SQLite file adds no meaningful disclosure.
Sync side: curl -o archive.db <host>:<port>/api/db.sqlite
"""
if not Path(DB_PATH).exists():
raise HTTPException(404, "archive db not present")
return FileResponse(
DB_PATH,
media_type="application/vnd.sqlite3",
filename="archive.db",
)
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
def index(): def index():
return INDEX_HTML return INDEX_HTML