imkrish commited on
Commit
7d3393c
·
verified ·
1 Parent(s): 8dec6cd

Whole-cluster backups + on-demand backup/restore endpoints

Browse files
Files changed (2) hide show
  1. app/backup.py +4 -3
  2. 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
- ["pg_dump", "-h", PGHOST, "-p", PG_PORT, "-U", PG_USER,
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", PG_DB, "-v", "ON_ERROR_STOP=0", "-q"],
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