import io import os import uuid from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile from fastapi.responses import FileResponse from PIL import Image from sqlalchemy import or_ from sqlalchemy.orm import Session from auth import get_current_user, get_user_from_token from database import get_db from models import Drink, DrinkCategory, User from schemas import DrinkCreate, DrinkResponse, DrinkUpdate router = APIRouter(prefix="/api/drinks", tags=["drinks"]) UPLOAD_DIR = os.path.join(os.path.dirname(__file__), "..", "uploads") os.makedirs(UPLOAD_DIR, exist_ok=True) ALLOWED_MIMES = {"image/jpeg", "image/png", "image/webp", "image/gif"} ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"} MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10 MB def get_user_drink(drink_id: int, user: User, db: Session) -> Drink: drink = db.query(Drink).filter(Drink.id == drink_id, Drink.owner_id == user.id).first() if not drink: raise HTTPException(status_code=404, detail="Drink not found") return drink @router.post("", response_model=DrinkResponse) async def create_drink( drink: DrinkCreate, user: User = Depends(get_current_user), db: Session = Depends(get_db), ): db_drink = Drink(**drink.model_dump(), owner_id=user.id) db.add(db_drink) db.commit() db.refresh(db_drink) return db_drink @router.get("", response_model=list[DrinkResponse]) def list_drinks( category: DrinkCategory | None = None, search: str | None = None, min_rating: float | None = Query(None, ge=0, le=5), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), user: User = Depends(get_current_user), db: Session = Depends(get_db), ): query = db.query(Drink).filter(Drink.owner_id == user.id) if category: query = query.filter(Drink.category == category) if search: search_term = f"%{search}%" query = query.filter( or_( Drink.name.ilike(search_term), Drink.region.ilike(search_term), Drink.producer.ilike(search_term), Drink.brewery.ilike(search_term), Drink.distillery.ilike(search_term), Drink.grape_variety.ilike(search_term), Drink.country.ilike(search_term), Drink.notes.ilike(search_term), ) ) if min_rating is not None: query = query.filter(Drink.rating >= min_rating) return query.order_by(Drink.created_at.desc()).offset(skip).limit(limit).all() @router.get("/{drink_id}", response_model=DrinkResponse) def get_drink( drink_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db), ): return get_user_drink(drink_id, user, db) @router.put("/{drink_id}", response_model=DrinkResponse) def update_drink( drink_id: int, updates: DrinkUpdate, user: User = Depends(get_current_user), db: Session = Depends(get_db), ): drink = get_user_drink(drink_id, user, db) update_data = updates.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(drink, field, value) db.commit() db.refresh(drink) return drink @router.delete("/{drink_id}") def delete_drink( drink_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db), ): drink = get_user_drink(drink_id, user, db) if drink.image_path: filepath = os.path.join(os.path.dirname(__file__), "..", drink.image_path) resolved = os.path.realpath(filepath) if resolved.startswith(os.path.realpath(UPLOAD_DIR)) and os.path.exists(resolved): os.remove(resolved) db.delete(drink) db.commit() return {"detail": "Drink deleted"} @router.post("/{drink_id}/upload-image", response_model=DrinkResponse) async def upload_image( drink_id: int, file: UploadFile = File(...), user: User = Depends(get_current_user), db: Session = Depends(get_db), ): drink = get_user_drink(drink_id, user, db) if file.content_type not in ALLOWED_MIMES: raise HTTPException( status_code=400, detail=f"Type de fichier non autorisé. Utilisez: {', '.join(ALLOWED_MIMES)}", ) ext = os.path.splitext(file.filename or "")[1].lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException( status_code=400, detail=f"Extension non autorisée. Utilisez: {', '.join(ALLOWED_EXTENSIONS)}", ) content = await file.read() if len(content) > MAX_UPLOAD_SIZE: raise HTTPException( status_code=400, detail=f"Fichier trop volumineux. Taille maximale: {MAX_UPLOAD_SIZE // (1024 * 1024)} MB", ) try: img = Image.open(io.BytesIO(content)) img.verify() real_format = img.format.lower() if img.format else "" if real_format not in {"jpeg", "png", "webp", "gif"}: raise HTTPException( status_code=400, detail=f"Format d'image non autorisé: {real_format}. Utilisez: JPEG, PNG, WebP ou GIF", ) except HTTPException: raise except Exception: raise HTTPException( status_code=400, detail="Le fichier n'est pas une image valide", ) filename = f"{uuid.uuid4().hex}{ext}" filepath = os.path.join(UPLOAD_DIR, filename) with open(filepath, "wb") as f: f.write(content) if drink.image_path: old_path = os.path.join(os.path.dirname(__file__), "..", drink.image_path) resolved = os.path.realpath(old_path) if resolved.startswith(os.path.realpath(UPLOAD_DIR)) and os.path.exists(resolved): os.remove(resolved) drink.image_path = f"uploads/{filename}" db.commit() db.refresh(drink) return drink MIME_MAP = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".gif": "image/gif", } @router.get("/{drink_id}/image") def get_drink_image( drink_id: int, token: str = Query(...), db: Session = Depends(get_db), ): user = get_user_from_token(token, db) drink = db.query(Drink).filter(Drink.id == drink_id, Drink.owner_id == user.id).first() if not drink or not drink.image_path: raise HTTPException(status_code=404, detail="Image non trouvée") filename = os.path.basename(drink.image_path) filepath = os.path.realpath(os.path.join(UPLOAD_DIR, filename)) if not filepath.startswith(os.path.realpath(UPLOAD_DIR)) or not os.path.isfile(filepath): raise HTTPException(status_code=404, detail="Image non trouvée") ext = os.path.splitext(filename)[1].lower() media_type = MIME_MAP.get(ext, "application/octet-stream") return FileResponse(filepath, media_type=media_type)