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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user