feat: add settings tabs, auto backup, and show latest 5 backups
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+68
-1
@@ -2,13 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
log = logging.getLogger("backup")
|
||||
|
||||
_BACKUP_NAME_RE = re.compile(r"^market_intel_\d{8}_\d{6}\.db$")
|
||||
DISPLAY_LIMIT = 5
|
||||
|
||||
|
||||
def _now_stamp(tz: str = "Asia/Shanghai") -> str:
|
||||
@@ -69,7 +73,7 @@ def restore_db(backup_path: Path, db_path: Path) -> None:
|
||||
src.close()
|
||||
|
||||
|
||||
def list_backups(backup_dir: Path) -> list[dict]:
|
||||
def list_backups(backup_dir: Path, limit: int | None = None) -> list[dict]:
|
||||
if not backup_dir.is_dir():
|
||||
return []
|
||||
items: list[dict] = []
|
||||
@@ -84,4 +88,67 @@ def list_backups(backup_dir: Path) -> list[dict]:
|
||||
"modified_ms": int(st.st_mtime * 1000),
|
||||
}
|
||||
)
|
||||
if limit is not None and len(items) >= limit:
|
||||
break
|
||||
return items
|
||||
|
||||
|
||||
def prune_backups(backup_dir: Path, keep: int) -> int:
|
||||
"""删除超出保留数量的旧备份,返回删除数。"""
|
||||
if keep < 1:
|
||||
return 0
|
||||
if not backup_dir.is_dir():
|
||||
return 0
|
||||
files = sorted(
|
||||
[
|
||||
p
|
||||
for p in backup_dir.glob("market_intel_*.db")
|
||||
if p.is_file() and is_valid_backup_name(p.name)
|
||||
],
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
removed = 0
|
||||
for p in files[keep:]:
|
||||
try:
|
||||
p.unlink()
|
||||
removed += 1
|
||||
except OSError as e:
|
||||
log.warning("failed to prune %s: %s", p, e)
|
||||
return removed
|
||||
|
||||
|
||||
def latest_backup_mtime_ms(backup_dir: Path) -> int | None:
|
||||
items = list_backups(backup_dir, limit=1)
|
||||
if not items:
|
||||
return None
|
||||
return int(items[0]["modified_ms"])
|
||||
|
||||
|
||||
def should_auto_backup(
|
||||
backup_dir: Path,
|
||||
*,
|
||||
enabled: bool,
|
||||
interval_hours: float,
|
||||
) -> bool:
|
||||
if not enabled:
|
||||
return False
|
||||
if interval_hours <= 0:
|
||||
return False
|
||||
latest = latest_backup_mtime_ms(backup_dir)
|
||||
if latest is None:
|
||||
return True
|
||||
age_ms = int(datetime.now().timestamp() * 1000) - latest
|
||||
return age_ms >= interval_hours * 3600 * 1000
|
||||
|
||||
|
||||
def run_auto_backup(
|
||||
db_path: Path,
|
||||
backup_dir: Path,
|
||||
*,
|
||||
tz: str = "Asia/Shanghai",
|
||||
keep: int = 30,
|
||||
) -> Path:
|
||||
dest = backup_db(db_path, backup_dir, tz=tz)
|
||||
prune_backups(backup_dir, keep)
|
||||
return dest
|
||||
|
||||
Reference in New Issue
Block a user