Files
autoclip/backend/tasks/data_cleanup.py
Kris Ka 15d1e94fe3 修复YouTube下载任务和项目删除功能
- 修复YouTube下载任务中asyncio导入缺失问题
- 修复项目删除功能中Task模型导入缺失和事务冲突问题
- 更新.gitignore文件,排除测试数据和媒体文件
- 添加数据库优化和清理相关脚本
- 完善项目删除功能,支持完整清理项目数据、文件和进度缓存
2025-09-15 15:00:55 +08:00

426 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
数据清理任务
定期清理数据库和文件系统中的过期数据
"""
import os
import logging
import shutil
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, Any, List
from celery import current_task, shared_task
from ..core.celery_app import celery_app
from ..core.database import SessionLocal
from ..models.task import Task, TaskStatus
from ..models.project import Project, ProjectStatus
from ..models.clip import Clip
from ..models.collection import Collection
from ..repositories.task_repository import TaskRepository
from ..repositories.project_repository import ProjectRepository
logger = logging.getLogger(__name__)
@shared_task(bind=True, name='backend.tasks.data_cleanup.cleanup_expired_data')
def cleanup_expired_data(self, days: int = 30) -> Dict[str, Any]:
"""
清理过期数据
Args:
days: 保留天数默认30天
Returns:
清理结果
"""
logger.info(f"开始清理过期数据,保留天数: {days}")
try:
# 创建数据库会话
db = SessionLocal()
try:
cleanup_results = {
'timestamp': datetime.utcnow().isoformat(),
'days': days,
'tasks_cleaned': 0,
'projects_cleaned': 0,
'files_cleaned': 0,
'errors': []
}
# 1. 清理过期任务
try:
task_repo = TaskRepository(db)
tasks_cleaned = task_repo.cleanup_old_tasks(days)
cleanup_results['tasks_cleaned'] = tasks_cleaned
logger.info(f"清理了 {tasks_cleaned} 个过期任务")
except Exception as e:
error_msg = f"清理任务失败: {str(e)}"
logger.error(error_msg)
cleanup_results['errors'].append(error_msg)
# 2. 清理过期项目
try:
projects_cleaned = _cleanup_expired_projects(db, days)
cleanup_results['projects_cleaned'] = projects_cleaned
logger.info(f"清理了 {projects_cleaned} 个过期项目")
except Exception as e:
error_msg = f"清理项目失败: {str(e)}"
logger.error(error_msg)
cleanup_results['errors'].append(error_msg)
# 3. 清理孤立文件
try:
files_cleaned = _cleanup_orphaned_files()
cleanup_results['files_cleaned'] = files_cleaned
logger.info(f"清理了 {files_cleaned} 个孤立文件")
except Exception as e:
error_msg = f"清理文件失败: {str(e)}"
logger.error(error_msg)
cleanup_results['errors'].append(error_msg)
# 4. 清理临时文件
try:
temp_files_cleaned = _cleanup_temp_files()
cleanup_results['temp_files_cleaned'] = temp_files_cleaned
logger.info(f"清理了 {temp_files_cleaned} 个临时文件")
except Exception as e:
error_msg = f"清理临时文件失败: {str(e)}"
logger.error(error_msg)
cleanup_results['errors'].append(error_msg)
cleanup_results['success'] = len(cleanup_results['errors']) == 0
cleanup_results['total_cleaned'] = (
cleanup_results['tasks_cleaned'] +
cleanup_results['projects_cleaned'] +
cleanup_results['files_cleaned'] +
cleanup_results.get('temp_files_cleaned', 0)
)
logger.info(f"数据清理完成,共清理 {cleanup_results['total_cleaned']} 项数据")
return cleanup_results
finally:
db.close()
except Exception as e:
logger.error(f"清理过期数据失败,错误: {e}")
raise
def _cleanup_expired_projects(db: SessionLocal, days: int) -> int:
"""清理过期项目"""
cutoff_date = datetime.utcnow() - timedelta(days=days)
# 查找过期的已完成项目
expired_projects = db.query(Project).filter(
Project.status == ProjectStatus.COMPLETED,
Project.updated_at < cutoff_date
).all()
cleaned_count = 0
for project in expired_projects:
try:
# 删除项目相关数据
_delete_project_data(db, project.id)
cleaned_count += 1
logger.info(f"清理过期项目: {project.id}")
except Exception as e:
logger.error(f"清理项目 {project.id} 失败: {e}")
return cleaned_count
def _delete_project_data(db: SessionLocal, project_id: str):
"""删除项目数据"""
# 删除相关任务
db.query(Task).filter(Task.project_id == project_id).delete()
# 删除相关切片
db.query(Clip).filter(Clip.project_id == project_id).delete()
# 删除相关合集
db.query(Collection).filter(Collection.project_id == project_id).delete()
# 删除项目记录
db.query(Project).filter(Project.id == project_id).delete()
# 删除项目文件
project_dir = Path(f"data/projects/{project_id}")
if project_dir.exists():
shutil.rmtree(project_dir)
# 清理进度数据
try:
from ..services.simple_progress import clear_progress
clear_progress(project_id)
except Exception as e:
logger.warning(f"清理进度数据失败: {e}")
db.commit()
def _cleanup_orphaned_files() -> int:
"""清理孤立文件"""
cleaned_count = 0
try:
# 获取数据库中的项目ID
db = SessionLocal()
try:
db_projects = {p.id for p in db.query(Project).all()}
finally:
db.close()
# 清理孤立的项目目录
projects_dir = Path("data/projects")
if projects_dir.exists():
for project_dir in projects_dir.iterdir():
if project_dir.is_dir() and project_dir.name not in db_projects:
if not project_dir.name.startswith('.'):
shutil.rmtree(project_dir)
cleaned_count += 1
logger.info(f"清理孤立项目目录: {project_dir.name}")
# 清理孤立的输出文件
output_dir = Path("data/output")
if output_dir.exists():
for file_path in output_dir.rglob("*"):
if file_path.is_file():
# 检查文件是否属于现有项目
file_name = file_path.name
is_orphaned = True
for project_id in db_projects:
if project_id in file_name:
is_orphaned = False
break
if is_orphaned:
file_path.unlink()
cleaned_count += 1
logger.info(f"清理孤立输出文件: {file_path}")
except Exception as e:
logger.error(f"清理孤立文件失败: {e}")
return cleaned_count
def _cleanup_temp_files() -> int:
"""清理临时文件"""
cleaned_count = 0
try:
temp_dir = Path("data/temp")
if temp_dir.exists():
for file_path in temp_dir.iterdir():
if file_path.is_file():
# 检查文件是否超过1小时
file_age = datetime.now() - datetime.fromtimestamp(file_path.stat().st_mtime)
if file_age > timedelta(hours=1):
file_path.unlink()
cleaned_count += 1
logger.info(f"清理临时文件: {file_path}")
# 清理处理中间文件
projects_dir = Path("data/projects")
if projects_dir.exists():
for project_dir in projects_dir.iterdir():
if project_dir.is_dir():
processing_dir = project_dir / "processing"
if processing_dir.exists():
for file_path in processing_dir.iterdir():
if file_path.is_file():
# 检查文件是否超过24小时
file_age = datetime.now() - datetime.fromtimestamp(file_path.stat().st_mtime)
if file_age > timedelta(hours=24):
file_path.unlink()
cleaned_count += 1
logger.info(f"清理处理中间文件: {file_path}")
except Exception as e:
logger.error(f"清理临时文件失败: {e}")
return cleaned_count
@shared_task(bind=True, name='backend.tasks.data_cleanup.check_data_consistency')
def check_data_consistency(self) -> Dict[str, Any]:
"""
检查数据一致性
Returns:
一致性检查结果
"""
logger.info("开始数据一致性检查")
try:
# 创建数据库会话
db = SessionLocal()
try:
issues = []
# 1. 检查项目数据一致性
db_projects = {p.id for p in db.query(Project).all()}
fs_projects = set()
projects_dir = Path("data/projects")
if projects_dir.exists():
for project_dir in projects_dir.iterdir():
if project_dir.is_dir() and not project_dir.name.startswith('.'):
fs_projects.add(project_dir.name)
# 检查孤立文件
orphaned_files = fs_projects - db_projects
if orphaned_files:
issues.append({
"type": "orphaned_files",
"count": len(orphaned_files),
"details": list(orphaned_files)
})
# 检查缺失文件
missing_files = db_projects - fs_projects
if missing_files:
issues.append({
"type": "missing_files",
"count": len(missing_files),
"details": list(missing_files)
})
# 2. 检查任务数据一致性
orphaned_tasks = db.query(Task).filter(
~Task.project_id.in_(db_projects)
).count()
if orphaned_tasks > 0:
issues.append({
"type": "orphaned_tasks",
"count": orphaned_tasks,
"details": []
})
# 3. 检查切片数据一致性
orphaned_clips = db.query(Clip).filter(
~Clip.project_id.in_(db_projects)
).count()
if orphaned_clips > 0:
issues.append({
"type": "orphaned_clips",
"count": orphaned_clips,
"details": []
})
# 4. 检查合集数据一致性
orphaned_collections = db.query(Collection).filter(
~Collection.project_id.in_(db_projects)
).count()
if orphaned_collections > 0:
issues.append({
"type": "orphaned_collections",
"count": orphaned_collections,
"details": []
})
return {
'timestamp': datetime.utcnow().isoformat(),
'total_issues': len(issues),
'issues': issues,
'status': 'healthy' if len(issues) == 0 else 'unhealthy'
}
finally:
db.close()
except Exception as e:
logger.error(f"数据一致性检查失败,错误: {e}")
raise
@shared_task(bind=True, name='backend.tasks.data_cleanup.cleanup_orphaned_data')
def cleanup_orphaned_data(self) -> Dict[str, Any]:
"""
清理孤立数据
Returns:
清理结果
"""
logger.info("开始清理孤立数据")
try:
# 创建数据库会话
db = SessionLocal()
try:
cleanup_results = {
'timestamp': datetime.utcnow().isoformat(),
'orphaned_tasks_cleaned': 0,
'orphaned_clips_cleaned': 0,
'orphaned_collections_cleaned': 0,
'orphaned_files_cleaned': 0
}
# 获取所有项目ID
db_projects = {p.id for p in db.query(Project).all()}
# 1. 清理孤立任务
orphaned_tasks = db.query(Task).filter(
~Task.project_id.in_(db_projects)
).all()
for task in orphaned_tasks:
db.delete(task)
cleanup_results['orphaned_tasks_cleaned'] += 1
logger.info(f"清理孤立任务: {task.id}")
# 2. 清理孤立切片
orphaned_clips = db.query(Clip).filter(
~Clip.project_id.in_(db_projects)
).all()
for clip in orphaned_clips:
db.delete(clip)
cleanup_results['orphaned_clips_cleaned'] += 1
logger.info(f"清理孤立切片: {clip.id}")
# 3. 清理孤立合集
orphaned_collections = db.query(Collection).filter(
~Collection.project_id.in_(db_projects)
).all()
for collection in orphaned_collections:
db.delete(collection)
cleanup_results['orphaned_collections_cleaned'] += 1
logger.info(f"清理孤立合集: {collection.id}")
# 4. 清理孤立文件
cleanup_results['orphaned_files_cleaned'] = _cleanup_orphaned_files()
db.commit()
total_cleaned = (
cleanup_results['orphaned_tasks_cleaned'] +
cleanup_results['orphaned_clips_cleaned'] +
cleanup_results['orphaned_collections_cleaned'] +
cleanup_results['orphaned_files_cleaned']
)
cleanup_results['total_cleaned'] = total_cleaned
cleanup_results['success'] = True
logger.info(f"孤立数据清理完成,共清理 {total_cleaned} 项数据")
return cleanup_results
finally:
db.close()
except Exception as e:
logger.error(f"清理孤立数据失败,错误: {e}")
raise