4e3a4b4602
带 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>
81 lines
2.5 KiB
Python
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
|