upload from file and url

This commit is contained in:
Nicola Malizia
2025-10-10 20:50:46 +02:00
parent 82a342ca25
commit a3a0bfc971
16 changed files with 466 additions and 35 deletions

View File

@@ -1,8 +1,13 @@
from sqlalchemy.orm import Session
import os
from sqlalchemy.orm import Session, joinedload
from . import models, schemas
def get_celebrity(db: Session, celebrity_id: int):
return db.query(models.Celebrity).filter(models.Celebrity.id == celebrity_id).first()
# Usiamo joinedload per caricare in anticipo le relazioni e evitare query N+1
return db.query(models.Celebrity).options(
joinedload(models.Celebrity.profile_image),
joinedload(models.Celebrity.images)
).filter(models.Celebrity.id == celebrity_id).first()
def get_celebrities(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Celebrity).offset(skip).limit(limit).all()
@@ -34,4 +39,49 @@ def delete_celebrity(db: Session, celebrity_id: int):
db.delete(db_celebrity)
db.commit()
return db_celebrity
return db_celebrity
# --- Funzioni CRUD per le Immagini ---
def get_image(db: Session, image_id: int):
return db.query(models.Image).filter(models.Image.id == image_id).first()
def create_celebrity_image(db: Session, celebrity_id: int, file_path: str):
db_image = models.Image(celebrity_id=celebrity_id, file_path=file_path)
db.add(db_image)
db.commit()
db.refresh(db_image)
return db_image
def set_profile_image(db: Session, celebrity_id: int, image_id: int):
db_celebrity = get_celebrity(db, celebrity_id)
db_image = get_image(db, image_id)
if not db_celebrity or not db_image or db_image.celebrity_id != celebrity_id:
return None
db_celebrity.profile_image_id = image_id
db.commit()
db.refresh(db_celebrity)
return get_celebrity(db, celebrity_id) # Ritorna con le relazioni aggiornate
def delete_image(db: Session, image_id: int):
db_image = get_image(db, image_id)
if not db_image:
return None
db_celebrity = get_celebrity(db, db_image.celebrity_id)
if db_celebrity and db_celebrity.profile_image_id == image_id:
db_celebrity.profile_image_id = None
db.add(db_celebrity)
try:
file_on_disk = os.path.join("uploads", db_image.file_path)
if os.path.exists(file_on_disk):
os.remove(file_on_disk)
except OSError as e:
print(f"Error deleting file {file_on_disk}: {e}")
db.delete(db_image)
db.commit()
return db_image

View File

@@ -1,8 +1,13 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from .routers import celebrities # Importa il nuovo router
app = FastAPI()
# Monta la directory 'uploads' per servire i file statici (immagini caricate)
# Ora saranno accessibili tramite l'URL /api/uploads/<nome_file>
app.mount("/api/uploads", StaticFiles(directory="uploads"), name="uploads")
# Includi il router delle celebrities nell'app principale
app.include_router(celebrities.router)

View File

@@ -1,6 +1,8 @@
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile
from sqlalchemy.orm import Session
from typing import List
import uuid
import shutil
from .. import crud, models, schemas
from ..database import get_db
@@ -13,7 +15,7 @@ router = APIRouter(
@router.post("/", response_model=schemas.Celebrity, status_code=201)
def create_celebrity(celebrity: schemas.CelebrityCreate, db: Session = Depends(get_db)):
# Qui potresti aggiungere un check per vedere se una celebrità con lo stesso nome esiste già
# Qui potresti aggiungere un check per vedere se una celebrità con lo stesso nome esiste già
return crud.create_celebrity(db=db, celebrity=celebrity)
@router.get("/", response_model=List[schemas.Celebrity])
@@ -40,4 +42,80 @@ def delete_celebrity(celebrity_id: int, db: Session = Depends(get_db)):
db_celebrity = crud.delete_celebrity(db, celebrity_id=celebrity_id)
if db_celebrity is None:
raise HTTPException(status_code=404, detail="Celebrity not found")
return db_celebrity
return db_celebrity
# --- NUOVI ENDPOINT PER LE IMMAGINI ---
@router.post("/{celebrity_id}/images", response_model=schemas.Image, status_code=201)
def upload_celebrity_image(celebrity_id: int, file: UploadFile = File(...), db: Session = Depends(get_db)):
db_celebrity = crud.get_celebrity(db, celebrity_id=celebrity_id)
if db_celebrity is None:
raise HTTPException(status_code=404, detail="Celebrity not found")
file_extension = file.filename.split('.')[-1]
unique_filename = f"{uuid.uuid4()}.{file_extension}"
file_location = f"uploads/{unique_filename}"
with open(file_location, "wb+") as file_object:
shutil.copyfileobj(file.file, file_object)
return crud.create_celebrity_image(db=db, celebrity_id=celebrity_id, file_path=unique_filename)
@router.post("/{celebrity_id}/images/from-url", response_model=schemas.Image, status_code=201)
async def add_image_from_url(celebrity_id: int, request: schemas.ImageUrlRequest, db: Session = Depends(get_db)):
db_celebrity = crud.get_celebrity(db, celebrity_id=celebrity_id)
if not db_celebrity:
raise HTTPException(status_code=404, detail="Celebrity not found")
url = str(request.url)
try:
async with httpx.AsyncClient() as client:
# Eseguiamo prima una richiesta HEAD per controllare il tipo e la dimensione
head_response = await client.head(url, follow_redirects=True, timeout=10)
head_response.raise_for_status()
content_type = head_response.headers.get('content-type')
if not content_type or not content_type.startswith('image/'):
raise HTTPException(status_code=400, detail="URL does not point to a valid image.")
# Limite di dimensione (es. 10MB)
content_length = int(head_response.headers.get('content-length', 0))
if content_length > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="Image size exceeds 10MB limit.")
# Scarica l'immagine
get_response = await client.get(url, follow_redirects=True, timeout=30)
get_response.raise_for_status()
# Determina l'estensione del file
file_extension = content_type.split('/')[-1]
if file_extension == 'jpeg': file_extension = 'jpg'
unique_filename = f"{uuid.uuid4()}.{file_extension}"
file_location = f"uploads/{unique_filename}"
with open(file_location, "wb") as file_object:
file_object.write(get_response.content)
return crud.create_celebrity_image(db=db, celebrity_id=celebrity_id, file_path=unique_filename)
except httpx.RequestError as exc:
raise HTTPException(status_code=400, detail=f"Failed to fetch image from URL: {exc}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"An internal error occurred: {e}")
@router.put("/{celebrity_id}/profile-image", response_model=schemas.Celebrity)
def set_celebrity_profile_image(celebrity_id: int, request: schemas.SetProfileImageRequest, db: Session = Depends(get_db)):
updated_celebrity = crud.set_profile_image(db, celebrity_id=celebrity_id, image_id=request.image_id)
if updated_celebrity is None:
raise HTTPException(status_code=404, detail="Celebrity or Image not found, or image does not belong to celebrity.")
return updated_celebrity
# NOTA: Un approccio pi├╣ RESTful sarebbe /api/images/{image_id} in un router dedicato.
# Per semplicità, lo inseriamo qui.
@router.delete("/images/{image_id}", response_model=schemas.Image)
def delete_image(image_id: int, db: Session = Depends(get_db)):
db_image = crud.delete_image(db, image_id=image_id)
if db_image is None:
raise HTTPException(status_code=404, detail="Image not found")
return db_image

View File

@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, HttpUrl
from typing import Optional, List
from datetime import date, datetime
from .models import GenderType, ShoeSystemType, BraSystemType, SurgeryType
@@ -74,6 +74,12 @@ class Image(ImageBase):
uploaded_at: datetime
class Config: from_attributes = True
# --- Richiesta per impostare l'immagine del profilo ---
class SetProfileImageRequest(BaseModel):
image_id: int
class ImageUrlRequest(BaseModel):
url: HttpUrl # Pydantic valida automaticamente che sia un URL valido
# =============================================================================
# SCHEMI PER CELEBRITY
@@ -107,7 +113,7 @@ class CelebrityBase(BaseModel):
official_website: Optional[str] = None
profile_image_id: Optional[int] = None
# Schema per la creazione di una nuova celebrità (eredita da Base)
# Schema per la creazione di una nuova celebrità (eredita da Base)
class CelebrityCreate(CelebrityBase):
pass
@@ -146,11 +152,12 @@ class Celebrity(CelebrityBase):
updated_at: datetime
# Campi relazionali che verranno popolati automaticamente da SQLAlchemy
profile_image: Optional[Image] = None
images: List[Image] = []
tattoos: List[Tattoo] = []
aliases: List[CelebrityAlias] = []
# Per semplicità, possiamo usare qui gli schemi di base,
# Per semplicità, possiamo usare qui gli schemi di base,
# ma in un'app reale potresti volere schemi specifici per la "lettura".
professions: List[Profession] = []
studios: List[Studio] = []

View File

@@ -1,6 +1,7 @@
# FastAPI e server
fastapi
uvicorn[standard]
python-multipart
# Pydantic per la validazione e le impostazioni
pydantic
@@ -9,4 +10,7 @@ pydantic-settings
# Database
sqlalchemy
psycopg2-binary
alembic
alembic
# Per richieste HTTP (scaricare immagini da URL)
httpx