# app/modules/cms/routes/api/store_media.py """ Store media and file management endpoints. Store Context: Uses token_store_id from JWT token (authenticated store API pattern). The get_current_store_api dependency guarantees token_store_id is present. """ import logging from fastapi import APIRouter, Depends, File, Query, UploadFile from sqlalchemy.orm import Session from app.api.deps import get_current_store_api from app.core.database import get_db from app.modules.cms.exceptions import MediaOptimizationException from app.modules.cms.schemas.media import ( FailedFileInfo, MediaDetailResponse, MediaItemResponse, MediaListResponse, MediaMetadataUpdate, MediaUploadResponse, MediaUsageResponse, MultipleUploadResponse, OptimizationResultResponse, UploadedFileInfo, ) from app.modules.cms.services.media_service import media_service from app.modules.tenancy.schemas.auth import UserContext store_media_router = APIRouter(prefix="/media") logger = logging.getLogger(__name__) @store_media_router.get("", response_model=MediaListResponse) def get_media_library( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), media_type: str | None = Query(None, description="image, video, document"), folder: str | None = Query(None, description="Filter by folder"), search: str | None = Query(None), current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """ Get store media library. - Get all media files for store - Filter by type (image, video, document) - Filter by folder - Search by filename - Support pagination """ media_files, total = media_service.get_media_library( db=db, store_id=current_user.token_store_id, skip=skip, limit=limit, media_type=media_type, folder=folder, search=search, ) return MediaListResponse( media=[MediaItemResponse.model_validate(m) for m in media_files], total=total, skip=skip, limit=limit, ) @store_media_router.post("/upload", response_model=MediaUploadResponse) async def upload_media( file: UploadFile = File(...), folder: str | None = Query("general", description="products, general, etc."), current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """ Upload media file. - Accept file upload - Validate file type and size - Store file in store-specific directory - Generate thumbnails for images - Save metadata to database - Return file URL """ # Read file content file_content = await file.read() # Upload using service (exceptions will propagate to handler) media_file = await media_service.upload_file( db=db, store_id=current_user.token_store_id, file_content=file_content, filename=file.filename or "unnamed", folder=folder or "general", ) db.commit() return MediaUploadResponse( id=media_file.id, file_url=media_file.file_url, thumbnail_url=media_file.thumbnail_url, filename=media_file.original_filename, file_size=media_file.file_size, media_type=media_file.media_type, message="File uploaded successfully", ) @store_media_router.post("/upload/multiple", response_model=MultipleUploadResponse) async def upload_multiple_media( files: list[UploadFile] = File(...), folder: str | None = Query("general"), current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """ Upload multiple media files at once. - Accept multiple files - Process each file - Return list of uploaded file URLs - Handle errors gracefully """ uploaded = [] failed = [] for file in files: try: file_content = await file.read() media_file = await media_service.upload_file( db=db, store_id=current_user.token_store_id, file_content=file_content, filename=file.filename or "unnamed", folder=folder or "general", ) uploaded.append(UploadedFileInfo( id=media_file.id, filename=media_file.original_filename or media_file.filename, file_url=media_file.file_url, thumbnail_url=media_file.thumbnail_url, )) except Exception as e: logger.warning(f"Failed to upload {file.filename}: {e}") failed.append(FailedFileInfo( filename=file.filename or "unnamed", error=str(e), )) db.commit() return MultipleUploadResponse( uploaded_files=uploaded, failed_files=failed, total_uploaded=len(uploaded), total_failed=len(failed), message=f"Uploaded {len(uploaded)} files, {len(failed)} failed", ) @store_media_router.get("/{media_id}", response_model=MediaDetailResponse) def get_media_details( media_id: int, current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """ Get media file details. - Get file metadata - Return file URL - Return basic info """ # Service will raise MediaNotFoundException if not found media = media_service.get_media( db=db, store_id=current_user.token_store_id, media_id=media_id, ) return MediaDetailResponse.model_validate(media) @store_media_router.put("/{media_id}", response_model=MediaDetailResponse) def update_media_metadata( media_id: int, metadata: MediaMetadataUpdate, current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """ Update media file metadata. - Update filename - Update alt text - Update description - Move to different folder """ # Service will raise MediaNotFoundException if not found media = media_service.update_media_metadata( db=db, store_id=current_user.token_store_id, media_id=media_id, filename=metadata.filename, alt_text=metadata.alt_text, description=metadata.description, folder=metadata.folder, metadata=metadata.metadata, ) db.commit() return MediaDetailResponse.model_validate(media) @store_media_router.delete("/{media_id}", response_model=MediaDetailResponse) def delete_media( media_id: int, current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """ Delete media file. - Verify file belongs to store - Delete file from storage - Delete database record - Return success/error """ # Service will raise MediaNotFoundException if not found media_service.delete_media( db=db, store_id=current_user.token_store_id, media_id=media_id, ) db.commit() return MediaDetailResponse(message="Media file deleted successfully") @store_media_router.get("/{media_id}/usage", response_model=MediaUsageResponse) def get_media_usage( media_id: int, current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """ Get where this media file is being used. Discovers usage from all registered module providers. """ # Verify media belongs to store (raises MediaNotFoundException if not found) media_service.get_media( db=db, store_id=current_user.token_store_id, media_id=media_id, ) # Discover usage from registered providers from app.modules.registry import MODULES usage = [] for module in MODULES.values(): provider = module.get_media_usage_provider_instance() if provider: usage.extend(provider.get_media_usage(db, media_id)) return MediaUsageResponse( media_id=media_id, usage=usage, total_usage_count=len(usage), ) @store_media_router.post("/optimize/{media_id}", response_model=OptimizationResultResponse) def optimize_media( media_id: int, current_user: UserContext = Depends(get_current_store_api), db: Session = Depends(get_db), ): """ Optimize media file (compress, resize, etc.). Note: Image optimization requires PIL/Pillow to be installed. """ # Service will raise MediaNotFoundException if not found media = media_service.get_media( db=db, store_id=current_user.token_store_id, media_id=media_id, ) if media.media_type != "image": raise MediaOptimizationException("Only images can be optimized") # For now, return current state - optimization is done on upload return OptimizationResultResponse( media_id=media_id, original_size=media.file_size, optimized_size=media.optimized_size or media.file_size, savings_percent=0.0 if not media.optimized_size else round((1 - media.optimized_size / media.file_size) * 100, 1), optimized_url=media.file_url, message="Image optimization applied on upload" if media.is_optimized else "Image not yet optimized", )