fix deletes
This commit is contained in:
parent
e741e0aaf0
commit
1a655f317f
|
|
@ -4,7 +4,7 @@ from typing import Optional
|
|||
|
||||
from fastapi import APIRouter, Query, status
|
||||
|
||||
from app.api.dependencies import CurrentUser, DatabaseSession
|
||||
from app.api.dependencies import CurrentUser, DatabaseSession, S3ClientDep
|
||||
from app.api.schemas import (
|
||||
BreadcrumbsResponse,
|
||||
FolderCreateRequest,
|
||||
|
|
@ -133,6 +133,7 @@ async def delete_folder(
|
|||
folder_id: str,
|
||||
current_user: CurrentUser,
|
||||
session: DatabaseSession,
|
||||
s3_client: S3ClientDep,
|
||||
recursive: bool = Query(False),
|
||||
):
|
||||
"""
|
||||
|
|
@ -142,9 +143,10 @@ async def delete_folder(
|
|||
folder_id: Folder ID
|
||||
current_user: Current authenticated user
|
||||
session: Database session
|
||||
recursive: If True, delete folder with all contents
|
||||
s3_client: S3 client (required for recursive deletion with assets)
|
||||
recursive: If True, recursively delete folder with all subfolders and assets
|
||||
"""
|
||||
folder_service = FolderService(session)
|
||||
folder_service = FolderService(session, s3_client)
|
||||
await folder_service.delete_folder(
|
||||
user_id=current_user.id,
|
||||
folder_id=folder_id,
|
||||
|
|
|
|||
|
|
@ -3,25 +3,32 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from loguru import logger
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.models import Folder
|
||||
from app.infra.s3_client import S3Client
|
||||
from app.repositories.asset_repository import AssetRepository
|
||||
from app.repositories.folder_repository import FolderRepository
|
||||
from app.repositories.user_repository import UserRepository
|
||||
|
||||
|
||||
class FolderService:
|
||||
"""Service for folder management operations (SOLID: Single Responsibility)."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
def __init__(self, session: AsyncSession, s3_client: Optional[S3Client] = None):
|
||||
"""
|
||||
Initialize folder service.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
s3_client: Optional S3 client for asset deletion
|
||||
"""
|
||||
self.folder_repo = FolderRepository(session)
|
||||
self.asset_repo = AssetRepository(session)
|
||||
self.user_repo = UserRepository(session)
|
||||
self.s3_client = s3_client
|
||||
self.session = session
|
||||
|
||||
async def create_folder(
|
||||
self,
|
||||
|
|
@ -161,33 +168,49 @@ class FolderService:
|
|||
"""
|
||||
Delete a folder.
|
||||
|
||||
IMPORTANT: Folder must be empty (no assets, no subfolders) to be deleted.
|
||||
Use AssetService or BatchOperationsService to delete assets first,
|
||||
or move them to another folder before deleting.
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
folder_id: Folder ID
|
||||
recursive: If True, delete folder with all subfolders (must still be empty of assets).
|
||||
If False, fail if folder has subfolders.
|
||||
recursive: If True, recursively delete folder with all subfolders and assets.
|
||||
If False, fail if folder has subfolders or assets.
|
||||
|
||||
Raises:
|
||||
HTTPException: If folder not found, not authorized, or not empty
|
||||
HTTPException: If folder not found, not authorized, or not empty (when recursive=False)
|
||||
"""
|
||||
folder = await self.get_folder(user_id, folder_id)
|
||||
|
||||
# Always check for assets (read-only query, acceptable for validation)
|
||||
asset_count = await self.asset_repo.count_in_folder(folder_id)
|
||||
if asset_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=(
|
||||
f"Folder contains {asset_count} assets. "
|
||||
"Please delete or move assets first using AssetService endpoints."
|
||||
),
|
||||
)
|
||||
if recursive:
|
||||
# Recursive deletion: delete all subfolders and assets
|
||||
if not self.s3_client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="S3 client required for recursive folder deletion",
|
||||
)
|
||||
|
||||
# Get all subfolders (deepest first)
|
||||
subfolders = await self.folder_repo.get_all_subfolders(folder_id)
|
||||
|
||||
# Delete assets in all subfolders (deepest first)
|
||||
for subfolder in reversed(subfolders):
|
||||
await self._delete_folder_assets(user_id, subfolder.id)
|
||||
await self.folder_repo.delete(subfolder)
|
||||
logger.info(f"Deleted subfolder {subfolder.id} ({subfolder.name})")
|
||||
|
||||
# Delete assets in the folder itself
|
||||
await self._delete_folder_assets(user_id, folder_id)
|
||||
|
||||
else:
|
||||
# Non-recursive: folder must be empty
|
||||
asset_count = await self.asset_repo.count_in_folder(folder_id)
|
||||
if asset_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=(
|
||||
f"Folder contains {asset_count} assets. "
|
||||
"Use recursive=true to delete folder with contents, or delete/move assets first."
|
||||
),
|
||||
)
|
||||
|
||||
if not recursive:
|
||||
# Check if folder has subfolders
|
||||
subfolders = await self.folder_repo.list_by_user(
|
||||
user_id=user_id,
|
||||
|
|
@ -202,24 +225,62 @@ class FolderService:
|
|||
),
|
||||
)
|
||||
|
||||
if recursive:
|
||||
# Delete all subfolders recursively (folders must be empty of assets)
|
||||
subfolders = await self.folder_repo.get_all_subfolders(folder_id)
|
||||
for subfolder in reversed(subfolders): # Delete from deepest to shallowest
|
||||
# Check that subfolder is empty of assets
|
||||
subfolder_asset_count = await self.asset_repo.count_in_folder(subfolder.id)
|
||||
if subfolder_asset_count > 0:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=(
|
||||
f"Subfolder '{subfolder.name}' contains {subfolder_asset_count} assets. "
|
||||
"All folders must be empty before deletion."
|
||||
),
|
||||
)
|
||||
await self.folder_repo.delete(subfolder)
|
||||
|
||||
# Delete the folder itself
|
||||
await self.folder_repo.delete(folder)
|
||||
logger.info(f"Deleted folder {folder_id} ({folder.name})")
|
||||
|
||||
async def _delete_folder_assets(self, user_id: str, folder_id: str) -> None:
|
||||
"""
|
||||
Delete all assets in a folder.
|
||||
|
||||
Moves files to trash bucket in S3, deletes from DB, and updates storage quota.
|
||||
|
||||
Args:
|
||||
user_id: User ID (for ownership verification)
|
||||
folder_id: Folder ID
|
||||
"""
|
||||
# Get all assets in folder
|
||||
assets = await self.asset_repo.list_by_folder(
|
||||
user_id=user_id,
|
||||
folder_id=folder_id,
|
||||
limit=10000, # Large limit to get all assets
|
||||
)
|
||||
|
||||
if not assets:
|
||||
return
|
||||
|
||||
total_bytes_freed = 0
|
||||
|
||||
# Delete each asset
|
||||
for asset in assets:
|
||||
try:
|
||||
# Move files to trash bucket
|
||||
self.s3_client.move_to_trash(asset.storage_key_original)
|
||||
if asset.storage_key_thumb:
|
||||
self.s3_client.move_to_trash(asset.storage_key_thumb)
|
||||
|
||||
# Track bytes for storage quota update
|
||||
total_bytes_freed += asset.size_bytes
|
||||
|
||||
# Delete from database
|
||||
await self.asset_repo.delete(asset)
|
||||
|
||||
logger.debug(f"Deleted asset {asset.id} from folder {folder_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete asset {asset.id} from folder {folder_id}: {e}")
|
||||
# Continue with other assets
|
||||
|
||||
# Update user's storage_used_bytes (free up quota)
|
||||
if total_bytes_freed > 0:
|
||||
user = await self.user_repo.get_by_id(user_id)
|
||||
if user:
|
||||
user.storage_used_bytes = max(0, user.storage_used_bytes - total_bytes_freed)
|
||||
await self.user_repo.update(user)
|
||||
logger.info(
|
||||
f"Freed {total_bytes_freed} bytes for user {user_id} "
|
||||
f"(deleted {len(assets)} assets from folder {folder_id})"
|
||||
)
|
||||
|
||||
async def get_breadcrumbs(self, user_id: str, folder_id: str) -> list[Folder]:
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue