feat: add settings tabs, auto backup, and show latest 5 backups
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user