from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response from fastapi.responses import JSONResponse from typing import List, Dict, Any, Optional import logging from datetime import datetime from dataclasses import dataclass import io from modules.auth import get_current_active_user, get_user_context from modules.configuration import APP_CONFIG # Import interfaces from modules.lucydom_interface import get_lucydom_interface, FileError, FileNotFoundError, FileStorageError, FilePermissionError, FileDeletionError from modules.lucydom_model import FileItem # Logger konfigurieren logger = logging.getLogger(__name__) # Alle Attribute des Models ermitteln (außer interne/spezielle Attribute) def get_model_attributes(model_class): return [attr for attr in dir(model_class) if not callable(getattr(model_class, attr)) and not attr.startswith('_') and attr != 'metadata' and attr != 'query' and attr != 'query_class' and attr != 'label' and attr != 'field_labels'] # Modell-Attribute für FileItem file_attributes = get_model_attributes(FileItem) @dataclass class AppContext: """Kontext-Objekt für alle benötigten Verbindungen und Benutzerinformationen""" mandate_id: int user_id: int interface_data: Any # LucyDOM Interface async def get_context(current_user: Dict[str, Any]) -> AppContext: """ Erstellt ein zentrales Kontext-Objekt mit allen benötigten Interfaces Args: current_user: Aktueller Benutzer aus der Authentifizierung Returns: AppContext-Objekt mit allen benötigten Verbindungen """ mandate_id, user_id = await get_user_context(current_user) interface_data = get_lucydom_interface(mandate_id, user_id) return AppContext( mandate_id=mandate_id, user_id=user_id, interface_data=interface_data ) # Router für Datei-Endpunkte erstellen router = APIRouter( prefix="/api/files", tags=["Files"], responses={ 404: {"description": "Not found"}, 400: {"description": "Bad request"}, 401: {"description": "Unauthorized"}, 403: {"description": "Forbidden"}, 500: {"description": "Internal server error"} } ) @router.get("", response_model=List[Dict[str, Any]]) async def get_files(current_user: Dict[str, Any] = Depends(get_current_active_user)): """Alle verfügbaren Dateien abrufen""" try: context = await get_context(current_user) # Alle Dateien generisch abrufen - nur Metadaten, keine Binärdaten files = context.interface_data.get_all_files() return files except Exception as e: logger.error(f"Fehler beim Abrufen der Dateien: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Fehler beim Abrufen der Dateien: {str(e)}" ) @router.post("/upload", status_code=status.HTTP_201_CREATED) async def upload_file( file: UploadFile = File(...), workflow_id: Optional[str] = Form(None), current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Upload einer Datei """ try: context = await get_context(current_user) # Datei einlesen file_content = await file.read() # Größenbeschränkung prüfen max_size = int(APP_CONFIG.get("File_Management_MAX_UPLOAD_SIZE_MB")) * 1024 * 1024 # in Bytes if len(file_content) > max_size: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail=f"Datei zu groß. Maximale Größe: {APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB" ) # Datei über das LucyDOM-Interface in der Datenbank speichern file_meta = context.interface_data.save_uploaded_file(file_content, file.filename) # Wenn workflow_id angegeben, aktualisiere die Dateiinformationen if workflow_id: update_data = {"workflow_id": workflow_id} context.interface_data.update_file(file_meta["id"], update_data) file_meta["workflow_id"] = workflow_id # Erfolgreiche Antwort return file_meta except FileStorageError as e: logger.error(f"Fehler beim Datei-Upload (Speichern): {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) except Exception as e: logger.error(f"Fehler beim Datei-Upload: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Fehler beim Datei-Upload: {str(e)}" ) @router.get("/{file_id}") async def get_file( file_id: str, current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Gibt eine Datei anhand ihrer ID zum Download zurück. Ruft sowohl Metadaten als auch Binärdaten ab. """ try: context = await get_context(current_user) # Datei über das LucyDOM-Interface aus der Datenbank abrufen # Verwendet die download_file-Methode, die nun Metadaten und Binärdaten kombiniert file_data = context.interface_data.download_file(file_id) # Datei zurückgeben headers = { "Content-Disposition": f'attachment; filename="{file_data["name"]}"' } return Response( content=file_data["content"], media_type=file_data["content_type"], headers=headers ) except FileNotFoundError as e: logger.warning(f"Datei nicht gefunden: {str(e)}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) except FilePermissionError as e: logger.warning(f"Keine Berechtigung für Datei: {str(e)}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e) ) except FileError as e: logger.error(f"Fehler beim Abrufen der Datei: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) except Exception as e: logger.error(f"Unerwarteter Fehler beim Abrufen der Datei: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Fehler beim Abrufen der Datei: {str(e)}" ) @router.delete("/{file_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_file( file_id: str, current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Löscht eine Datei anhand ihrer ID aus der Datenbank. Entfernt sowohl die Metadaten als auch die Binärdaten. """ try: context = await get_context(current_user) # Datei über das LucyDOM-Interface löschen # Die Methode kümmert sich nun um das Löschen beider Tabellen (files und file_data) context.interface_data.delete_file(file_id) # Erfolgreiche Löschung ohne Inhalt zurückgeben (204 No Content) return Response(status_code=status.HTTP_204_NO_CONTENT) except FileNotFoundError as e: logger.warning(f"Datei nicht gefunden: {str(e)}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) except FilePermissionError as e: logger.warning(f"Keine Berechtigung zum Löschen der Datei: {str(e)}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e) ) except FileDeletionError as e: logger.error(f"Fehler beim Löschen der Datei: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) except Exception as e: logger.error(f"Unerwarteter Fehler beim Löschen der Datei: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Fehler beim Löschen der Datei: {str(e)}" ) @router.get("/stats", response_model=Dict[str, Any]) async def get_file_stats( current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Gibt Statistiken über die gespeicherten Dateien zurück. """ try: context = await get_context(current_user) # Alle Dateien abrufen - nur Metadaten all_files = context.interface_data.get_all_files() # Statistiken berechnen total_files = len(all_files) total_size = sum(file.get("size", 0) for file in all_files) # Nach Dateityp gruppieren file_types = {} for file in all_files: file_type = file.get("mime_type", "unknown").split("/")[0] if file_type not in file_types: file_types[file_type] = 0 file_types[file_type] += 1 return { "total_files": total_files, "total_size_bytes": total_size, "file_types": file_types } except Exception as e: logger.error(f"Fehler beim Abrufen der Datei-Statistiken: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Fehler beim Abrufen der Datei-Statistiken: {str(e)}" )