CoolFace
Apppublic

imkrish/remote-postgres

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
main.py262 linesDownload Raw Back to app
1import os2import json3import time4import secrets5import threading6from datetime import datetime, timedelta7 8import psycopg9import gradio as gr10 11import backup12from fastapi import FastAPI, Depends, HTTPException, status13from fastapi.responses import JSONResponse14from fastapi.security import HTTPBasic, HTTPBasicCredentials15 16PG_USER = os.environ.get("POSTGRES_USER", "demo")17PG_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "")18PG_DB = os.environ.get("POSTGRES_DB", "demo")19PG_PORT = os.environ.get("PGPORT", "5432")20 21# If set, the Gradio UI and the JSON API require this password (username: admin).22APP_PASSWORD = os.environ.get("APP_PASSWORD", "")23APP_USER = os.environ.get("APP_USER", "admin")24 25TUNNEL_FILE = "/tmp/tunnel.json"   # written by start.sh (bore or ngrok)26 27BOOT = datetime.now()28HISTORY: list[tuple[str, bool, bool]] = []   # (HH:MM:SS, postgres_up, tunnel_up)29HISTORY_MAX = 50030 31# Keep-alive counter (incremented by the in-container loop AND any external cron).32KEEPALIVE = {"count": 0, "last": None, "src": "—"}33 34 35# ---------------------------------------------------------------- helpers ----36def get_tunnel():37    """Return (host, port) of the public TCP tunnel from /tmp/tunnel.json, or None."""38    try:39        with open(TUNNEL_FILE) as f:40            d = json.load(f)41        if d.get("host") and d.get("port"):42            return d["host"], str(d["port"])43    except Exception:44        return None45    return None46 47 48def pg_ok() -> bool:49    try:50        with psycopg.connect(51            host="/tmp", port=PG_PORT, user=PG_USER,52            dbname=PG_DB, password=PG_PASSWORD, connect_timeout=3,53        ):54            return True55    except Exception:56        return False57 58 59def build_url(tunnel):60    if not tunnel:61        return None62    host, port = tunnel63    return f"postgresql://{PG_USER}:{PG_PASSWORD}@{host}:{port}/{PG_DB}"64 65 66def humanize(delta: timedelta) -> str:67    s = int(delta.total_seconds())68    d, s = divmod(s, 86400)69    h, s = divmod(s, 3600)70    m, s = divmod(s, 60)71    if d:72        return f"{d}d {h}h {m}m"73    if h:74        return f"{h}h {m}m {s}s"75    return f"{m}m {s}s"76 77 78def record_check():79    ok = pg_ok()80    tn = get_tunnel() is not None81    HISTORY.append((datetime.now().strftime("%H:%M:%S"), ok, tn))82    del HISTORY[:-HISTORY_MAX]83    return ok, tn84 85 86def _watcher_loop():87    while True:88        try:89            record_check()90        except Exception:91            pass92        time.sleep(30)93 94 95# Background uptime watcher — records a sample every 30s while the container lives.96threading.Thread(target=_watcher_loop, daemon=True).start()97 98# Periodic Postgres backups (to BACKUP_DIR and/or a private HF Dataset).99backup.start_scheduler()100 101 102# ------------------------------------------------------------- JSON / API ----103api = FastAPI(title="Remote Postgres", docs_url=None, redoc_url=None)104security = HTTPBasic(auto_error=False)105 106 107def require_auth(credentials: HTTPBasicCredentials | None = Depends(security)):108    if not APP_PASSWORD:109        return110    if credentials is None or not (111        secrets.compare_digest(credentials.username, APP_USER)112        and secrets.compare_digest(credentials.password, APP_PASSWORD)113    ):114        raise HTTPException(115            status_code=status.HTTP_401_UNAUTHORIZED,116            detail="Authentication required",117            headers={"WWW-Authenticate": "Basic"},118        )119 120 121@api.get("/health")122async def health():123    """Open endpoint — used by the self keep-alive and external uptime monitors."""124    tunnel = get_tunnel()125    ok = pg_ok()126    return JSONResponse({127        "status": "ok" if ok else "degraded",128        "postgres": ok,129        "tunnel": bool(tunnel),130        "uptime": humanize(datetime.now() - BOOT),131    })132 133 134@api.get("/keepalive")135async def keepalive(src: str = "self"):136    """Hit to reset HF's idle timer (by the in-container loop AND any external cron).137    Open + logged so you can confirm pings are landing."""138    KEEPALIVE["count"] += 1139    KEEPALIVE["last"] = datetime.now()140    KEEPALIVE["src"] = src141    print(f"[keepalive] hit #{KEEPALIVE['count']} from '{src}' at "142          f"{KEEPALIVE['last'].strftime('%Y-%m-%d %H:%M:%S')}", flush=True)143    return {"ok": True, "count": KEEPALIVE["count"], "src": src}144 145 146@api.get("/api/connection")147async def connection(_: None = Depends(require_auth)):148    tunnel = get_tunnel()149    host, port = tunnel if tunnel else (None, None)150    return {151        "postgres_up": pg_ok(),152        "connection_url": build_url(tunnel),153        "host": host, "port": port,154        "user": PG_USER, "password": PG_PASSWORD, "database": PG_DB,155    }156 157 158@api.post("/admin/backup")159async def admin_backup(_: None = Depends(require_auth)):160    try:161        path = backup.backup()162        return {"ok": True, "file": os.path.basename(path)}163    except Exception as e:164        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)165 166 167@api.post("/admin/restore")168async def admin_restore(_: None = Depends(require_auth)):169    """Reload the latest backup (whole cluster). Cleanest on a fresh/empty DB;170    on a populated DB it best-effort merges and skips rows that already exist."""171    try:172        done = backup.restore()173        return {"ok": bool(done)}174    except Exception as e:175        return JSONResponse({"ok": False, "error": str(e)}, status_code=500)176 177 178# ------------------------------------------------- Gradio uptime watcher ----179def snapshot():180    tunnel = get_tunnel()181    up = pg_ok()182    url = build_url(tunnel) or "— tunnel starting… refresh in a few seconds —"183    host, port = tunnel if tunnel else ("—", "—")184 185    samples = len(HISTORY)186    pct = (100 * sum(1 for _, o, _ in HISTORY if o) / samples) if samples else 100.0187    status_md = (188        f"### Status\n"189        f"- **Postgres:** {'🟢 up' if up else '🔴 down'}\n"190        f"- **Public tunnel:** {'🟢 up' if tunnel else '🟡 off'}\n"191        f"- **Container uptime:** {humanize(datetime.now() - BOOT)}\n"192        f"- **PG availability (this container):** {pct:.1f}%  ·  {samples} checks\n"193        f"- **Keep-alive hits:** {KEEPALIVE['count']}  ·  last from "194        f"`{KEEPALIVE['src']}` at "195        f"{KEEPALIVE['last'].strftime('%H:%M:%S') if KEEPALIVE['last'] else 'never'}\n"196        f"- **Backups:** {backup.STATE['count']} taken  ·  last: "197        f"{backup.STATE['last'].strftime('%H:%M:%S') if backup.STATE['last'] else 'none yet'}"198        f"  ·  HF off-Space: {'on' if backup.STATE['hf'] else 'off'}\n"199        f"- **Last checked:** {datetime.now().strftime('%H:%M:%S')}"200    )201    table = [202        [t, "✅" if o else "❌", "✅" if n else "❌"]203        for (t, o, n) in reversed(HISTORY[-25:])204    ]205    return status_md, url, host, port, PG_USER, PG_PASSWORD, PG_DB, table206 207 208with gr.Blocks(title="Remote Postgres — Uptime Watcher", theme=gr.themes.Soft()) as demo:209    gr.Markdown("# 🐘 Remote Postgres — Uptime Watcher")210    gr.Markdown(211        "Throwaway Postgres for demo projects, exposed over a TCP tunnel (bore). "212        "Data is backed up, but the URL **changes on every restart**."213    )214    status_box = gr.Markdown()215    url_box = gr.Textbox(216        label="Connection URL — paste into your projects",217        interactive=False, show_copy_button=True,218    )219    with gr.Row():220        host_box = gr.Textbox(label="Host", interactive=False)221        port_box = gr.Textbox(label="Port", interactive=False)222    with gr.Row():223        user_box = gr.Textbox(label="User", interactive=False)224        pw_box = gr.Textbox(label="Password", interactive=False)225        db_box = gr.Textbox(label="Database", interactive=False)226    history_box = gr.Dataframe(227        headers=["time", "postgres", "tunnel"],228        label="Recent checks (newest first)",229        interactive=False,230    )231    with gr.Row():232        refresh_btn = gr.Button("Refresh now", variant="primary")233        backup_btn = gr.Button("Back up now")234        restore_btn = gr.Button("Restore latest")235 236    def backup_now():237        try:238            backup.backup()239        except Exception as e:240            print(f"[ui] manual backup failed: {e}", flush=True)241        return snapshot()242 243    def restore_now():244        try:245            backup.restore()246        except Exception as e:247            print(f"[ui] manual restore failed: {e}", flush=True)248        return snapshot()249 250    outputs = [status_box, url_box, host_box, port_box,251               user_box, pw_box, db_box, history_box]252    timer = gr.Timer(10)253    timer.tick(snapshot, outputs=outputs)254    refresh_btn.click(snapshot, outputs=outputs)255    backup_btn.click(backup_now, outputs=outputs)256    restore_btn.click(restore_now, outputs=outputs)257    demo.load(snapshot, outputs=outputs)258 259 260auth = (APP_USER, APP_PASSWORD) if APP_PASSWORD else None261app = gr.mount_gradio_app(api, demo, path="/", auth=auth)262