feat: add settings tabs, auto backup, and show latest 5 backups

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-02 09:15:21 +08:00
parent 52ebbfbeae
commit 458cc42dd5
12 changed files with 621 additions and 190 deletions
+65 -3
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import re
import time
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
@@ -10,7 +11,16 @@ 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
from packages.db.backup import (
DISPLAY_LIMIT,
backup_db,
ensure_backup_dir,
latest_backup_mtime_ms,
list_backups,
prune_backups,
resolve_backup_file,
restore_db,
)
router = APIRouter(
prefix="/settings",
@@ -36,6 +46,12 @@ class RestoreBody(BaseModel):
backup_name: str = Field(min_length=1, max_length=128)
class AutoBackupBody(BaseModel):
current_password: str = Field(min_length=1, max_length=256)
enabled: bool
interval_hours: float = Field(default=24, ge=1, le=168)
def _verify_password_or_401(current_password: str) -> None:
s = get_settings()
if auth_disabled():
@@ -44,6 +60,25 @@ def _verify_password_or_401(current_password: str) -> None:
raise HTTPException(status_code=401, detail="current password incorrect")
def _backup_meta(s) -> dict:
backup_dir = s.backup_dir_path
latest_ms = latest_backup_mtime_ms(backup_dir)
next_due_ms = None
if s.backup_auto_enabled and s.backup_interval_hours > 0:
if latest_ms is None:
next_due_ms = int(time.time() * 1000)
else:
next_due_ms = latest_ms + int(s.backup_interval_hours * 3600 * 1000)
return {
"auto_enabled": s.backup_auto_enabled,
"interval_hours": s.backup_interval_hours,
"keep_count": s.backup_keep_count,
"display_limit": DISPLAY_LIMIT,
"latest_backup_ms": latest_ms,
"next_due_ms": next_due_ms,
}
@router.get("/backup")
def get_backup_info() -> dict:
s = get_settings()
@@ -55,9 +90,9 @@ def get_backup_info() -> dict:
probe.write_text("", encoding="utf-8")
probe.unlink(missing_ok=True)
writable = True
items = list_backups(backup_dir)
items = list_backups(backup_dir, limit=DISPLAY_LIMIT)
except OSError as e:
items = list_backups(backup_dir) if backup_dir.is_dir() else []
items = list_backups(backup_dir, limit=DISPLAY_LIMIT) if backup_dir.is_dir() else []
if not items:
raise HTTPException(status_code=500, detail=f"backup dir error: {e}") from e
return {
@@ -65,6 +100,32 @@ def get_backup_info() -> dict:
"db_path": str(s.db_path),
"writable": writable,
"backups": items,
**_backup_meta(s),
}
@router.put("/backup/auto")
def update_auto_backup(body: AutoBackupBody) -> dict:
_verify_password_or_401(body.current_password)
s = get_settings()
updates = {
"BACKUP_AUTO_ENABLED": "1" if body.enabled else "0",
"BACKUP_INTERVAL_HOURS": str(
int(body.interval_hours)
if float(body.interval_hours).is_integer()
else body.interval_hours
),
}
try:
update_env_file(s.env_file_path, updates)
except OSError as e:
raise HTTPException(status_code=500, detail=f"failed to write {s.env_file_path}: {e}") from e
reload_settings()
info = get_backup_info()
return {
"ok": True,
"message": "自动备份设置已保存",
**info,
}
@@ -74,6 +135,7 @@ def create_backup(body: BackupActionBody) -> dict:
s = get_settings()
try:
dest = backup_db(s.db_path, s.backup_dir_path, tz=s.tz)
prune_backups(s.backup_dir_path, s.backup_keep_count)
except FileNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
except OSError as e: