Spaces:
Paused
Paused
Whole-cluster backups + on-demand backup/restore endpoints
Browse files- app/backup.py +4 -3
- app/main.py +29 -0
app/backup.py
CHANGED
|
@@ -60,9 +60,9 @@ def backup():
|
|
| 60 |
name = f"backup-{_ts()}.sql.gz"
|
| 61 |
path = os.path.join(BACKUP_DIR, name)
|
| 62 |
try:
|
|
|
|
| 63 |
dump = subprocess.run(
|
| 64 |
-
["
|
| 65 |
-
"-d", PG_DB, "--clean", "--if-exists"],
|
| 66 |
stdout=subprocess.PIPE, check=True,
|
| 67 |
).stdout
|
| 68 |
with gzip.open(path, "wb") as f:
|
|
@@ -114,10 +114,11 @@ def restore():
|
|
| 114 |
return False
|
| 115 |
|
| 116 |
print(f"[backup] restoring from {src}", flush=True)
|
|
|
|
| 117 |
with gzip.open(src, "rb") as f:
|
| 118 |
subprocess.run(
|
| 119 |
["psql", "-h", PGHOST, "-p", PG_PORT, "-U", PG_USER,
|
| 120 |
-
"-d",
|
| 121 |
input=f.read(), check=True,
|
| 122 |
)
|
| 123 |
print("[backup] restore complete", flush=True)
|
|
|
|
| 60 |
name = f"backup-{_ts()}.sql.gz"
|
| 61 |
path = os.path.join(BACKUP_DIR, name)
|
| 62 |
try:
|
| 63 |
+
# whole-cluster dump: every database + roles (not just one DB)
|
| 64 |
dump = subprocess.run(
|
| 65 |
+
["pg_dumpall", "-h", PGHOST, "-p", PG_PORT, "-U", PG_USER],
|
|
|
|
| 66 |
stdout=subprocess.PIPE, check=True,
|
| 67 |
).stdout
|
| 68 |
with gzip.open(path, "wb") as f:
|
|
|
|
| 114 |
return False
|
| 115 |
|
| 116 |
print(f"[backup] restoring from {src}", flush=True)
|
| 117 |
+
# connect to 'postgres'; the dumpall script \connects into each database itself
|
| 118 |
with gzip.open(src, "rb") as f:
|
| 119 |
subprocess.run(
|
| 120 |
["psql", "-h", PGHOST, "-p", PG_PORT, "-U", PG_USER,
|
| 121 |
+
"-d", "postgres", "-v", "ON_ERROR_STOP=0", "-q"],
|
| 122 |
input=f.read(), check=True,
|
| 123 |
)
|
| 124 |
print("[backup] restore complete", flush=True)
|
app/main.py
CHANGED
|
@@ -155,6 +155,26 @@ async def connection(_: None = Depends(require_auth)):
|
|
| 155 |
}
|
| 156 |
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
# ------------------------------------------------- Gradio uptime watcher ----
|
| 159 |
def snapshot():
|
| 160 |
tunnel = get_tunnel()
|
|
@@ -211,6 +231,7 @@ with gr.Blocks(title="Remote Postgres — Uptime Watcher", theme=gr.themes.Soft(
|
|
| 211 |
with gr.Row():
|
| 212 |
refresh_btn = gr.Button("Refresh now", variant="primary")
|
| 213 |
backup_btn = gr.Button("Back up now")
|
|
|
|
| 214 |
|
| 215 |
def backup_now():
|
| 216 |
try:
|
|
@@ -219,12 +240,20 @@ with gr.Blocks(title="Remote Postgres — Uptime Watcher", theme=gr.themes.Soft(
|
|
| 219 |
print(f"[ui] manual backup failed: {e}", flush=True)
|
| 220 |
return snapshot()
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
outputs = [status_box, url_box, host_box, port_box,
|
| 223 |
user_box, pw_box, db_box, history_box]
|
| 224 |
timer = gr.Timer(10)
|
| 225 |
timer.tick(snapshot, outputs=outputs)
|
| 226 |
refresh_btn.click(snapshot, outputs=outputs)
|
| 227 |
backup_btn.click(backup_now, outputs=outputs)
|
|
|
|
| 228 |
demo.load(snapshot, outputs=outputs)
|
| 229 |
|
| 230 |
|
|
|
|
| 155 |
}
|
| 156 |
|
| 157 |
|
| 158 |
+
@api.post("/admin/backup")
|
| 159 |
+
async 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")
|
| 168 |
+
async 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 ----
|
| 179 |
def snapshot():
|
| 180 |
tunnel = get_tunnel()
|
|
|
|
| 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:
|
|
|
|
| 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 |
|