feat: add storage cleanup mechanism with soft delete and auto scheduler

- Add soft delete (deleted_at column) to preserve task records for statistics
- Implement cleanup service to delete old files while keeping DB records
- Add automatic cleanup scheduler (configurable interval, default 24h)
- Add admin endpoints: storage stats, cleanup trigger, scheduler status
- Update task service with admin views (include deleted/files_deleted)
- Add frontend storage management UI in admin dashboard
- Add i18n translations for storage management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-14 12:41:01 +08:00
parent 81a0a3ab0f
commit 73112db055
23 changed files with 1359 additions and 634 deletions

View File

@@ -65,7 +65,7 @@ class TaskService:
return task
def get_task_by_id(
self, db: Session, task_id: str, user_id: int
self, db: Session, task_id: str, user_id: int, include_deleted: bool = False
) -> Optional[Task]:
"""
Get task by ID with user isolation
@@ -74,16 +74,20 @@ class TaskService:
db: Database session
task_id: Task ID (UUID)
user_id: User ID (for isolation)
include_deleted: If True, include soft-deleted tasks
Returns:
Task object or None if not found/unauthorized
"""
task = (
db.query(Task)
.filter(and_(Task.task_id == task_id, Task.user_id == user_id))
.first()
query = db.query(Task).filter(
and_(Task.task_id == task_id, Task.user_id == user_id)
)
return task
# Filter out soft-deleted tasks by default
if not include_deleted:
query = query.filter(Task.deleted_at.is_(None))
return query.first()
def get_user_tasks(
self,
@@ -97,6 +101,7 @@ class TaskService:
limit: int = 50,
order_by: str = "created_at",
order_desc: bool = True,
include_deleted: bool = False,
) -> Tuple[List[Task], int]:
"""
Get user's tasks with pagination and filtering
@@ -112,6 +117,7 @@ class TaskService:
limit: Pagination limit
order_by: Sort field (created_at, updated_at, completed_at)
order_desc: Sort descending
include_deleted: If True, include soft-deleted tasks
Returns:
Tuple of (tasks list, total count)
@@ -119,6 +125,10 @@ class TaskService:
# Base query with user isolation
query = db.query(Task).filter(Task.user_id == user_id)
# Filter out soft-deleted tasks by default
if not include_deleted:
query = query.filter(Task.deleted_at.is_(None))
# Apply status filter
if status:
query = query.filter(Task.status == status)
@@ -244,7 +254,9 @@ class TaskService:
self, db: Session, task_id: str, user_id: int
) -> bool:
"""
Delete task with user isolation
Soft delete task with user isolation.
Sets deleted_at timestamp instead of removing record.
Database records are preserved for statistics tracking.
Args:
db: Database session
@@ -252,17 +264,18 @@ class TaskService:
user_id: User ID (for isolation)
Returns:
True if deleted, False if not found/unauthorized
True if soft deleted, False if not found/unauthorized
"""
task = self.get_task_by_id(db, task_id, user_id)
if not task:
return False
# Cascade delete will handle task_files
db.delete(task)
# Soft delete: set deleted_at timestamp
task.deleted_at = datetime.utcnow()
task.updated_at = datetime.utcnow()
db.commit()
logger.info(f"Deleted task {task_id} for user {user_id}")
logger.info(f"Soft deleted task {task_id} for user {user_id}")
return True
def _cleanup_old_tasks(
@@ -389,6 +402,82 @@ class TaskService:
"failed": failed,
}
def get_all_tasks_admin(
self,
db: Session,
user_id: Optional[int] = None,
status: Optional[TaskStatus] = None,
include_deleted: bool = True,
include_files_deleted: bool = True,
skip: int = 0,
limit: int = 50,
order_by: str = "created_at",
order_desc: bool = True,
) -> Tuple[List[Task], int]:
"""
Get all tasks for admin view (no user isolation).
Includes soft-deleted tasks by default.
Args:
db: Database session
user_id: Filter by user ID (optional)
status: Filter by status (optional)
include_deleted: Include soft-deleted tasks (default True)
include_files_deleted: Include tasks with deleted files (default True)
skip: Pagination offset
limit: Pagination limit
order_by: Sort field
order_desc: Sort descending
Returns:
Tuple of (tasks list, total count)
"""
query = db.query(Task)
# Optional user filter
if user_id is not None:
query = query.filter(Task.user_id == user_id)
# Filter soft-deleted if requested
if not include_deleted:
query = query.filter(Task.deleted_at.is_(None))
# Filter file-deleted if requested
if not include_files_deleted:
query = query.filter(Task.file_deleted == False)
# Apply status filter
if status:
query = query.filter(Task.status == status)
# Get total count
total = query.count()
# Apply sorting
sort_column = getattr(Task, order_by, Task.created_at)
if order_desc:
query = query.order_by(desc(sort_column))
else:
query = query.order_by(sort_column)
# Apply pagination
tasks = query.offset(skip).limit(limit).all()
return tasks, total
def get_task_by_id_admin(self, db: Session, task_id: str) -> Optional[Task]:
"""
Get task by ID for admin (no user isolation, includes deleted).
Args:
db: Database session
task_id: Task ID (UUID)
Returns:
Task object or None if not found
"""
return db.query(Task).filter(Task.task_id == task_id).first()
# Global service instance
task_service = TaskService()