mirror of
https://github.com/zhouxiaoka/autoclip.git
synced 2026-09-03 06:24:14 +08:00
- 修复YouTube下载任务中asyncio导入缺失问题 - 修复项目删除功能中Task模型导入缺失和事务冲突问题 - 更新.gitignore文件,排除测试数据和媒体文件 - 添加数据库优化和清理相关脚本 - 完善项目删除功能,支持完整清理项目数据、文件和进度缓存
426 lines
15 KiB
Python
426 lines
15 KiB
Python
"""
|
||
数据清理任务
|
||
定期清理数据库和文件系统中的过期数据
|
||
"""
|
||
|
||
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
|