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:
|
||||
|
||||
+30
-3
@@ -1,4 +1,4 @@
|
||||
"""Worker 入口:周期回填到期结算指数。"""
|
||||
"""Worker 入口:周期回填到期结算指数 + 自动备份。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,8 +10,9 @@ from typing import Any
|
||||
|
||||
from apps.collector.okx_rest import OkxRestClient
|
||||
from apps.worker.settle import backfill_settlements
|
||||
from packages.config import get_settings
|
||||
from packages.config import get_settings, reload_settings
|
||||
from packages.db import Repository
|
||||
from packages.db.backup import run_auto_backup, should_auto_backup
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@@ -29,13 +30,35 @@ def _handle_signal(signum: int, _frame: Any) -> None:
|
||||
_STOP = True
|
||||
|
||||
|
||||
def _maybe_auto_backup() -> None:
|
||||
s = reload_settings()
|
||||
if not should_auto_backup(
|
||||
s.backup_dir_path,
|
||||
enabled=s.backup_auto_enabled,
|
||||
interval_hours=s.backup_interval_hours,
|
||||
):
|
||||
return
|
||||
try:
|
||||
dest = run_auto_backup(
|
||||
s.db_path,
|
||||
s.backup_dir_path,
|
||||
tz=s.tz,
|
||||
keep=s.backup_keep_count,
|
||||
)
|
||||
log.info("auto backup ok: %s", dest.name)
|
||||
except Exception as e: # noqa: BLE001
|
||||
log.exception("auto backup failed: %s", e)
|
||||
|
||||
|
||||
def run() -> int:
|
||||
settings = get_settings()
|
||||
interval = max(60, int(settings.settle_backfill_interval_sec))
|
||||
log.info(
|
||||
"start settle backfill interval=%ss db=%s",
|
||||
"start settle backfill interval=%ss db=%s backup_auto=%s every=%sh",
|
||||
interval,
|
||||
settings.db_path,
|
||||
settings.backup_auto_enabled,
|
||||
settings.backup_interval_hours,
|
||||
)
|
||||
repo = Repository(settings.db_path)
|
||||
client = OkxRestClient(
|
||||
@@ -43,6 +66,8 @@ def run() -> int:
|
||||
proxy=settings.okx_proxy or None,
|
||||
)
|
||||
try:
|
||||
# 启动时先尝试一次自动备份(若到期)
|
||||
_maybe_auto_backup()
|
||||
while not _STOP:
|
||||
try:
|
||||
result = backfill_settlements(
|
||||
@@ -62,6 +87,8 @@ def run() -> int:
|
||||
except Exception as e: # noqa: BLE001
|
||||
log.exception("backfill loop failed: %s", e)
|
||||
|
||||
_maybe_auto_backup()
|
||||
|
||||
end = time.monotonic() + interval
|
||||
while not _STOP and time.monotonic() < end:
|
||||
time.sleep(min(1.0, end - time.monotonic()))
|
||||
|
||||
Reference in New Issue
Block a user