feat: add database backup and restore in system settings
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""系统设置:管理员账号。"""
|
||||
"""系统设置:管理员账号、数据备份与恢复。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,6 +10,7 @@ from pydantic import BaseModel, Field
|
||||
from apps.api.auth import auth_disabled, require_auth, verify_credentials
|
||||
from packages.config import get_settings, reload_settings
|
||||
from packages.config.env_file import update_env_file
|
||||
from packages.db.backup import backup_db, ensure_backup_dir, list_backups, resolve_backup_file, restore_db
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/settings",
|
||||
@@ -26,6 +27,87 @@ class UpdateAccountBody(BaseModel):
|
||||
new_password: str | None = Field(default=None, max_length=256)
|
||||
|
||||
|
||||
class BackupActionBody(BaseModel):
|
||||
current_password: str = Field(min_length=1, max_length=256)
|
||||
|
||||
|
||||
class RestoreBody(BaseModel):
|
||||
current_password: str = Field(min_length=1, max_length=256)
|
||||
backup_name: str = Field(min_length=1, max_length=128)
|
||||
|
||||
|
||||
def _verify_password_or_401(current_password: str) -> None:
|
||||
s = get_settings()
|
||||
if auth_disabled():
|
||||
raise HTTPException(status_code=400, detail="auth disabled")
|
||||
if not verify_credentials(s.admin_username, current_password):
|
||||
raise HTTPException(status_code=401, detail="current password incorrect")
|
||||
|
||||
|
||||
@router.get("/backup")
|
||||
def get_backup_info() -> dict:
|
||||
s = get_settings()
|
||||
backup_dir = s.backup_dir_path
|
||||
writable = False
|
||||
try:
|
||||
ensure_backup_dir(backup_dir)
|
||||
probe = backup_dir / ".write_probe"
|
||||
probe.write_text("", encoding="utf-8")
|
||||
probe.unlink(missing_ok=True)
|
||||
writable = True
|
||||
items = list_backups(backup_dir)
|
||||
except OSError as e:
|
||||
items = list_backups(backup_dir) if backup_dir.is_dir() else []
|
||||
if not items:
|
||||
raise HTTPException(status_code=500, detail=f"backup dir error: {e}") from e
|
||||
return {
|
||||
"backup_dir": str(backup_dir),
|
||||
"db_path": str(s.db_path),
|
||||
"writable": writable,
|
||||
"backups": items,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/backup")
|
||||
def create_backup(body: BackupActionBody) -> dict:
|
||||
_verify_password_or_401(body.current_password)
|
||||
s = get_settings()
|
||||
try:
|
||||
dest = backup_db(s.db_path, s.backup_dir_path, tz=s.tz)
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e)) from e
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=500, detail=f"backup failed: {e}") from e
|
||||
st = dest.stat()
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "备份完成",
|
||||
"name": dest.name,
|
||||
"path": str(dest),
|
||||
"size_bytes": st.st_size,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/restore")
|
||||
def restore_backup(body: RestoreBody) -> dict:
|
||||
_verify_password_or_401(body.current_password)
|
||||
s = get_settings()
|
||||
try:
|
||||
backup_path = resolve_backup_file(s.backup_dir_path, body.backup_name.strip())
|
||||
restore_db(backup_path, s.db_path)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e)) from e
|
||||
except OSError as e:
|
||||
raise HTTPException(status_code=500, detail=f"restore failed: {e}") from e
|
||||
return {
|
||||
"ok": True,
|
||||
"message": f"已从 {body.backup_name} 恢复数据库",
|
||||
"restored_from": body.backup_name,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/account")
|
||||
def get_account() -> dict:
|
||||
s = get_settings()
|
||||
|
||||
Reference in New Issue
Block a user