feat: backup & restore built into the admin panel
Neither Remnawave nor Marzban has this natively (community tools only, per docs.rw's own comparison table) — one-click download of a tar.gz with a consistent SQLite snapshot (via sqlite3's backup API, safe even under WAL) plus .env, and upload-to-restore from the same file. Restore validates the archive is real (gzip + tar structure), that mbs.db is an actual sqlite database with the expected tables (not just any file named mbs.db), and rejects oversized uploads — before touching anything live. Takes a timestamped safety copy of the current db/.env before overwriting, clears stale -wal/-shm siblings so the restored file doesn't get replayed against the wrong WAL, and restarts mbs-bot automatically when .env was part of the restore (api.py isn't restarted from within its own request handler for the obvious reason). Verified with a full round-trip test: backup -> mutate state -> restore -> confirm the mutation is reverted, plus three negative cases (garbage data, oversized upload, a fake non-sqlite mbs.db) all correctly rejected with no side effects. Needs python-multipart for FastAPI's UploadFile — added to requirements.txt, picked up by the next 'mbs update'. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
cfac7c1445
commit
f586cf9fa3
4 changed files with 188 additions and 1 deletions
32
api.py
32
api.py
|
|
@ -5,11 +5,12 @@ import os
|
|||
import re
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from fastapi import FastAPI, HTTPException, Request, Response
|
||||
from fastapi import FastAPI, HTTPException, Request, Response, File, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi import Body
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, FileResponse
|
||||
|
||||
import backup
|
||||
import db
|
||||
import links
|
||||
import nodeprov
|
||||
|
|
@ -490,6 +491,35 @@ def admin_set_bot_settings(request: Request, body: dict = Body(...)):
|
|||
return {"ok": True, "username": username, "restarted": restarted}
|
||||
|
||||
|
||||
@app.get("/admin/api/backup")
|
||||
def admin_download_backup(request: Request):
|
||||
require_admin(request)
|
||||
data = backup.create_backup()
|
||||
filename = f"mbs-backup-{datetime.datetime.utcnow().strftime('%Y%m%d-%H%M%S')}.tar.gz"
|
||||
return Response(
|
||||
content=data, media_type="application/gzip",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/admin/api/backup/restore")
|
||||
async def admin_restore_backup(request: Request, file: UploadFile = File(...)):
|
||||
require_admin(request)
|
||||
data = await file.read()
|
||||
try:
|
||||
result = backup.restore_backup(data)
|
||||
except backup.RestoreError as e:
|
||||
raise HTTPException(400, str(e))
|
||||
restarted_bot = False
|
||||
if result["restored_env"]:
|
||||
try:
|
||||
subprocess.run(["systemctl", "restart", "mbs-bot"], check=True, timeout=15)
|
||||
restarted_bot = True
|
||||
except Exception:
|
||||
restarted_bot = False
|
||||
return {"ok": True, **result, "restarted_bot": restarted_bot}
|
||||
|
||||
|
||||
@app.get("/admin/api/stats")
|
||||
def admin_stats(request: Request):
|
||||
require_admin(request)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue