from datetime import datetime, timedelta, timezone from typing import List, Optional from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile from sqlalchemy.orm import Session from app.core.deps import get_current_user from app.db.session import get_db from app.models import Asset, User from app.schemas import AssetResponse, AssetTrashRequest from app.services.storage import asset_storage router = APIRouter(prefix="/api/assets", tags=["assets"]) ALLOWED_TYPES = { "image": ["image/jpeg", "image/png", "image/webp"], "video": ["video/mp4", "video/webm", "video/quicktime"], "audio": ["audio/mpeg", "audio/mp4", "audio/wav", "audio/ogg", "audio/x-wav"], "pose_sheet": ["image/jpeg", "image/png", "image/webp"], } MAX_SIZE_BYTES = 500 * 1024 * 1024 @router.post("/upload", response_model=AssetResponse, status_code=201) async def upload_asset( file: UploadFile = File(...), asset_type: str = Form(...), db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): if asset_type not in ALLOWED_TYPES: raise HTTPException(400, f"asset_type must be one of {list(ALLOWED_TYPES.keys())}") mime = file.content_type or "" if mime not in ALLOWED_TYPES[asset_type]: raise HTTPException(400, f"Unsupported mime type {mime} for {asset_type}") subfolder = f"{current_user.id}/{asset_type}" storage_path, size_bytes = await asset_storage.save_upload(file, subfolder) if size_bytes > MAX_SIZE_BYTES: raise HTTPException(413, "File too large (max 500MB)") thumbnail_path = None width = height = None duration_seconds = None if asset_type in ("image", "pose_sheet"): thumbnail_path = asset_storage.generate_thumbnail(storage_path, f"{current_user.id}/thumbs") try: from PIL import Image abs_path = asset_storage.absolute_path(storage_path) with Image.open(abs_path) as image: width, height = image.size except Exception: pass elif asset_type == "video": thumbnail_path = asset_storage.generate_video_thumbnail(storage_path, f"{current_user.id}/thumbs") duration_seconds = asset_storage.detect_duration_seconds(storage_path) else: duration_seconds = asset_storage.detect_duration_seconds(storage_path) asset = Asset( owner_id=current_user.id, asset_type=asset_type, mime_type=mime, original_filename=file.filename or "upload", storage_path=storage_path, thumbnail_path=thumbnail_path, size_bytes=size_bytes, width=width, height=height, duration_seconds=duration_seconds, ) db.add(asset) db.commit() db.refresh(asset) return asset @router.get("/", response_model=List[AssetResponse]) def list_assets( asset_type: Optional[str] = None, include_trashed: bool = False, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): query = db.query(Asset).filter(Asset.owner_id == current_user.id) if not include_trashed: query = query.filter(Asset.is_trashed.is_(False)) if asset_type: query = query.filter(Asset.asset_type == asset_type) return query.order_by(Asset.created_at.desc()).all() @router.post("/trash", status_code=200) def move_assets_to_trash( payload: AssetTrashRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ): if not payload.asset_ids: raise HTTPException(400, "No asset ids provided") assets = ( db.query(Asset) .filter(Asset.owner_id == current_user.id, Asset.id.in_(payload.asset_ids)) .all() ) if not assets: raise HTTPException(404, "No matching assets found") delete_after_at = datetime.now(timezone.utc) + timedelta(days=30) for asset in assets: asset.is_trashed = True asset.delete_after_at = delete_after_at db.commit() return { "moved_to_trash": len(assets), "delete_after_at": delete_after_at.isoformat(), }