feat: add database backup and restore in system settings
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""SQLite 在线备份与恢复。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
_BACKUP_NAME_RE = re.compile(r"^market_intel_\d{8}_\d{6}\.db$")
|
||||
|
||||
|
||||
def _now_stamp(tz: str = "Asia/Shanghai") -> str:
|
||||
return datetime.now(ZoneInfo(tz)).strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
|
||||
def ensure_backup_dir(path: Path) -> Path:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def is_valid_backup_name(name: str) -> bool:
|
||||
return bool(_BACKUP_NAME_RE.fullmatch(name))
|
||||
|
||||
|
||||
def resolve_backup_file(backup_dir: Path, name: str) -> Path:
|
||||
if not is_valid_backup_name(name):
|
||||
raise ValueError("invalid backup filename")
|
||||
path = (backup_dir / name).resolve()
|
||||
root = backup_dir.resolve()
|
||||
if not str(path).startswith(str(root)):
|
||||
raise ValueError("invalid backup path")
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(name)
|
||||
return path
|
||||
|
||||
|
||||
def backup_db(db_path: Path, backup_dir: Path, tz: str = "Asia/Shanghai") -> Path:
|
||||
ensure_backup_dir(backup_dir)
|
||||
if not db_path.is_file():
|
||||
raise FileNotFoundError(f"database not found: {db_path}")
|
||||
|
||||
dest = backup_dir / f"market_intel_{_now_stamp(tz)}.db"
|
||||
src = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
dst = sqlite3.connect(str(dest))
|
||||
try:
|
||||
src.backup(dst)
|
||||
finally:
|
||||
dst.close()
|
||||
finally:
|
||||
src.close()
|
||||
return dest
|
||||
|
||||
|
||||
def restore_db(backup_path: Path, db_path: Path) -> None:
|
||||
if not backup_path.is_file():
|
||||
raise FileNotFoundError(f"backup not found: {backup_path}")
|
||||
|
||||
src = sqlite3.connect(str(backup_path))
|
||||
try:
|
||||
dst = sqlite3.connect(str(db_path))
|
||||
try:
|
||||
src.backup(dst)
|
||||
finally:
|
||||
dst.close()
|
||||
finally:
|
||||
src.close()
|
||||
|
||||
|
||||
def list_backups(backup_dir: Path) -> list[dict]:
|
||||
if not backup_dir.is_dir():
|
||||
return []
|
||||
items: list[dict] = []
|
||||
for p in sorted(backup_dir.glob("market_intel_*.db"), reverse=True):
|
||||
if not p.is_file() or not is_valid_backup_name(p.name):
|
||||
continue
|
||||
st = p.stat()
|
||||
items.append(
|
||||
{
|
||||
"name": p.name,
|
||||
"size_bytes": st.st_size,
|
||||
"modified_ms": int(st.st_mtime * 1000),
|
||||
}
|
||||
)
|
||||
return items
|
||||
Reference in New Issue
Block a user