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 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")
PORT = int(os.environ.get("PORT", "8765"))
@@ -218,6 +218,23 @@ def stats():
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)
def index():
return INDEX_HTML