ay/backend/routers/document.py
2025-07-01 18:25:10 +02:00

187 lines
No EOL
9.2 KiB
Python

# backend/routers/document.py
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
import os
import uuid # For generating unique filenames
import logging
from typing import List # Required for list type hint in get_user_documents
from core.database import get_db
# Removed unused 'create_access_token'
from core.security import get_current_user # Ensure this is the correct import for your get_current_user dependency
from core.config import settings # To access upload directory
from crud import document as crud_document
# Removed unused 'crud_user' as it's not directly used in this router
from schemas import document as schemas_document
from schemas import user as schemas_user # For UserResponse schema
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/documents",
tags=["Documents"],
responses={404: {"description": "Not found"}},
)
@router.post("/upload-cv", response_model=schemas_document.DocumentResponse, status_code=status.HTTP_201_CREATED, summary="Uploader un CV")
async def upload_cv(
file: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: schemas_user.UserResponse = Depends(get_current_user)
):
"""
Permet à un utilisateur authentifié d'uploader un CV.
Le fichier est stocké sur le serveur et ses métadonnées sont enregistrées en base de données.
"""
logger.info(f"Tentative d'upload de CV par l'utilisateur {current_user.id} - Nom du fichier: {file.filename}")
if not file.filename:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Le nom du fichier est manquant.")
allowed_extensions = ('.pdf', '.doc', '.docx')
# Use os.path.splitext to safely get the extension
file_extension = os.path.splitext(file.filename)[1].lower()
if file_extension not in allowed_extensions:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Seuls les fichiers {', '.join(allowed_extensions).upper()} sont autorisés."
)
upload_dir = settings.UPLOADS_DIR # Utilisez le chemin absolu configuré dans settings
os.makedirs(upload_dir, exist_ok=True)
# Generate a unique filename using UUID to prevent collisions and potential path traversal issues
unique_filename = f"{uuid.uuid4()}{file_extension}"
file_path = os.path.join(upload_dir, unique_filename)
try:
# Write the file in chunks for efficiency with large files
with open(file_path, "wb") as buffer:
while content := await file.read(1024 * 1024): # Read in 1MB chunks
buffer.write(content)
logger.info(f"Fichier '{file.filename}' enregistré sous '{file_path}' pour l'utilisateur {current_user.id}")
except Exception as e:
logger.error(f"Erreur lors de l'enregistrement du fichier {file.filename}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de l'enregistrement du fichier: {e}"
)
finally:
# Ensure the UploadFile is closed even if an error occurs
await file.close()
# Save document metadata in the database
# The DocumentCreate schema might not need 'filename' as a field if you pass it directly to crud
# Assuming DocumentCreate schema only takes filename and crud.create_document handles filepath
document_data = schemas_document.DocumentCreate(filename=file.filename)
db_document = crud_document.create_document(db, document_data, file_path, current_user.id)
logger.info(f"Document ID {db_document.id} créé en base de données pour l'utilisateur {current_user.id}")
return db_document
@router.get("/", response_model=List[schemas_document.DocumentResponse], summary="Lister les documents de l'utilisateur")
def get_user_documents(
db: Session = Depends(get_db),
current_user: schemas_user.UserResponse = Depends(get_current_user)
):
"""
Récupère tous les documents uploadés par l'utilisateur authentifié.
"""
logger.info(f"Tentative de listage des documents pour l'utilisateur {current_user.id}")
documents = crud_document.get_documents_by_owner(db, current_user.id)
return documents
@router.get("/{document_id}", response_model=schemas_document.DocumentResponse, summary="Récupérer un document par ID")
def get_document_details(
document_id: int,
db: Session = Depends(get_db),
current_user: schemas_user.UserResponse = Depends(get_current_user)
):
"""
Récupère les détails d'un document spécifique appartenant à l'utilisateur courant.
"""
logger.info(f"Tentative de récupération du document {document_id} pour l'utilisateur {current_user.id}")
# Appel à la fonction CRUD qui filtre déjà par owner_id
document = crud_document.get_document_by_id(db, document_id, current_user.id)
if not document:
# Si le document n'est pas trouvé (soit il n'existe pas, soit il n'appartient pas à cet utilisateur)
logger.warning(f"Document {document_id} non trouvé ou non autorisé pour l'utilisateur {current_user.id}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Document non trouvé ou vous n'avez pas l'autorisation d'y accéder."
)
return document
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Supprimer un document par ID")
async def delete_document(
document_id: int,
db: Session = Depends(get_db),
current_user: schemas_user.UserResponse = Depends(get_current_user)
):
"""
Supprime un document spécifique de l'utilisateur authentifié,
à la fois de la base de données et du système de fichiers.
"""
logger.info(f"Tentative de suppression du document {document_id} pour l'utilisateur {current_user.id}")
# Appel à la fonction CRUD qui filtre déjà par owner_id
db_document = crud_document.get_document_by_id(db, document_id, current_user.id)
if not db_document:
# Si le document n'est pas trouvé (soit il n'existe pas, soit il n'appartient pas à cet utilisateur)
logger.warning(f"Document {document_id} non trouvé ou non autorisé pour la suppression par l'utilisateur {current_user.id}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Document non trouvé ou vous n'avez pas la permission de le supprimer."
)
# Supprimer le fichier du système de fichiers s'il existe et si un chemin est défini
if db_document.filepath and os.path.exists(db_document.filepath):
try:
os.remove(db_document.filepath)
logger.info(f"Fichier physique '{db_document.filepath}' supprimé pour le document {document_id}.")
except OSError as e:
logger.error(f"Erreur lors de la suppression du fichier physique '{db_document.filepath}' pour le document {document_id}: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erreur lors de la suppression du fichier sur le serveur: {e}"
)
else:
logger.warning(f"Le document {document_id} n'a pas de chemin de fichier ou le fichier n'existe pas: {db_document.filepath}")
# Supprimer l'entrée de la base de données
success = crud_document.delete_document(db, document_id)
if not success:
logger.error(f"Échec de la suppression de l'entrée du document {document_id} de la base de données.")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Échec de la suppression du document de la base de données.")
logger.info(f"Document {document_id} et son fichier physique supprimés avec succès pour l'utilisateur {current_user.id}.")
return {} # 204 No Content typically returns an empty body
# Optional: Add a route to download the actual file if needed
@router.get("/{document_id}/download", summary="Télécharger un document")
async def download_document(
document_id: int,
db: Session = Depends(get_db),
current_user: schemas_user.UserResponse = Depends(get_current_user)
):
"""
Permet à l'utilisateur authentifié de télécharger un de ses documents.
"""
logger.info(f"Tentative de téléchargement du document {document_id} par l'utilisateur {current_user.id}")
db_document = crud_document.get_document_by_id(db, document_id, current_user.id)
if not db_document:
logger.warning(f"Document {document_id} non trouvé ou non autorisé pour le téléchargement par l'utilisateur {current_user.id}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Document non trouvé ou non autorisé.")
if not os.path.exists(db_document.filepath):
logger.error(f"Fichier physique non trouvé pour le document {document_id} à l'emplacement: {db_document.filepath}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Fichier physique non trouvé sur le serveur.")
# Return the file as a FastAPI FileResponse
return FileResponse(path=db_document.filepath, filename=db_document.filename, media_type="application/octet-stream")