# 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")