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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user