Files
system-backu/backend/app/core/retention.py
T
publ 4e3a4b4602 feat: 数据备份管理系统 v0.1.0
带 Web 操作界面的备份系统,支持:
- 数据源:MySQL 数据库(mysqldump)、服务器目录(tar.gz)
- 存储目标:本地目录、S3 兼容对象存储(MinIO/Ceph/AWS S3)
- 触发方式:手动 + Cron 定时调度(APScheduler)
- 备份还原:MySQL 库、目录
- JWT 登录认证 + Fernet 字段加密
- 保留策略:按数量 + 按天数双重清理

技术栈:FastAPI + SQLAlchemy 2 + APScheduler + aioboto3;
前端 Vite + React 18 + TypeScript + Ant Design 5 + Zustand。
Docker Compose 一键起。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-22 18:27:41 +08:00

81 lines
2.5 KiB
Python

"""备份保留策略清理。"""
from datetime import datetime, timedelta
from typing import Optional
from sqlalchemy.orm import Session
from app.core.storage.factory import create_storage
from app.models.job import Job
from app.models.run import Run
from app.utils.logging import get_logger
log = get_logger(__name__)
def apply_retention(db: Session, job: Job) -> int:
"""根据 job 的 retention_count / retention_days 清理旧备份。
返回删除的 run 数。
"""
# 取该 job 所有成功 runs,按 finished_at 倒序
runs = (
db.query(Run)
.filter(Run.job_id == job.id, Run.status == "success")
.order_by(Run.finished_at.desc())
.all()
)
if not runs:
return 0
keep_count = job.retention_count or 7
keep_days = job.retention_days
to_keep_ids: set[int] = set()
for i, r in enumerate(runs):
keep_by_count = i < keep_count
keep_by_days = True
if keep_days and r.finished_at:
threshold = datetime.utcnow() - timedelta(days=keep_days)
if r.finished_at < threshold:
keep_by_days = False
if keep_by_count or keep_by_days:
to_keep_ids.add(r.id)
to_delete = [r for r in runs if r.id not in to_keep_ids]
if not to_delete:
return 0
storage = create_storage(job.storage)
removed = 0
for r in to_delete:
try:
if r.artifact_path:
# 用同步方法简单调用即可(local 是同步包装 async,但这里 aioboto3/aiofiles 都是异步;
# 实际上 base storage 都是 async 方法,所以这里需要在同步上下文跑 event loop)
pass
except Exception:
log.exception("Failed to delete artifact %s (db row still kept)", r.artifact_path)
db.delete(r)
removed += 1
db.commit()
log.info("Retention cleanup for job=%s: deleted %s runs", job.name, removed)
# 真实删除 S3/local 文件(需要在 async 上下文)
import asyncio
try:
loop = asyncio.new_event_loop()
try:
for r in to_delete:
if r.artifact_path:
try:
loop.run_until_complete(storage.delete(r.artifact_path))
except Exception:
log.exception("Artifact delete failed: %s", r.artifact_path)
finally:
loop.close()
except Exception:
log.exception("Artifact cleanup loop error")
return removed