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 getCurrentActiveUser, getUserContext from modules.configuration import APP_CONFIG # Import interfaces from modules.lucydomInterface import getLucydomInterface, FileError, FileNotFoundError, FileStorageError, FilePermissionError, FileDeletionError from modules.lucydomModel import FileItem # Configure logger logger = logging.getLogger(__name__) # Get all attributes of the model def getModelAttributes(modelClass): return [attr for attr in dir(modelClass) if not callable(getattr(modelClass, attr)) and not attr.startswith('_') and attr not in ('metadata', 'query', 'query_class', 'label', 'field_labels')] # Model attributes for FileItem fileAttributes = getModelAttributes(FileItem) @dataclass class AppContext: """Context object for all required connections and user information""" mandateId: int userId: int interfaceData: Any # LucyDOM Interface async def getContext(currentUser: Dict[str, Any]) -> AppContext: """Creates a central context object with all required connections""" mandateId, userId = await getUserContext(currentUser) interfaceData = getLucydomInterface(mandateId, userId) return AppContext( mandateId=mandateId, userId=userId, interfaceData=interfaceData ) # Create router for file endpoints 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 getFiles(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)): """Get all available files""" try: context = await getContext(currentUser) # Get all files generically - only metadata, no binary data files = context.interfaceData.getAllFiles() return files except Exception as e: logger.error(f"Error retrieving files: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error retrieving files: {str(e)}" ) @router.post("/upload", status_code=status.HTTP_201_CREATED) async def uploadFile( file: UploadFile = File(...), workflowId: Optional[str] = Form(None), currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) ): """Upload a file""" try: context = await getContext(currentUser) # Read file fileContent = await file.read() # Check size limits maxSize = int(APP_CONFIG.get("File_Management_MAX_UPLOAD_SIZE_MB")) * 1024 * 1024 # in bytes if len(fileContent) > maxSize: raise HTTPException( status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, detail=f"File too large. Maximum size: {APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB" ) # Save file via LucyDOM interface in the database fileMeta = context.interfaceData.saveUploadedFile(fileContent, file.filename) # If workflowId is provided, update the file information if workflowId: updateData = {"workflowId": workflowId} context.interfaceData.updateFile(fileMeta["id"], updateData) fileMeta["workflowId"] = workflowId # Successful response return fileMeta except FileStorageError as e: logger.error(f"Error during file upload (storage): {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) except Exception as e: logger.error(f"Error during file upload: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error during file upload: {str(e)}" ) @router.get("/{fileId}") async def getFile( fileId: str, currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) ): """Returns a file by its ID for download""" try: context = await getContext(currentUser) # Get file via LucyDOM interface from the database fileData = context.interfaceData.downloadFile(fileId) # Return file headers = { "Content-Disposition": f'attachment; filename="{fileData["name"]}"' } return Response( content=fileData["content"], media_type=fileData["contentType"], headers=headers ) except FileNotFoundError as e: logger.warning(f"File not found: {str(e)}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) except FilePermissionError as e: logger.warning(f"No permission for file: {str(e)}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e) ) except FileError as e: logger.error(f"Error retrieving file: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) except Exception as e: logger.error(f"Unexpected error retrieving file: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error retrieving file: {str(e)}" ) @router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT) async def deleteFile( fileId: str, currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) ): """Deletes a file by its ID from the database""" try: context = await getContext(currentUser) # Delete file via LucyDOM interface context.interfaceData.deleteFile(fileId) # Return successful deletion without content (204 No Content) return Response(status_code=status.HTTP_204_NO_CONTENT) except FileNotFoundError as e: logger.warning(f"File not found: {str(e)}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) except FilePermissionError as e: logger.warning(f"No permission to delete file: {str(e)}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e) ) except FileDeletionError as e: logger.error(f"Error deleting file: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) except Exception as e: logger.error(f"Unexpected error deleting file: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error deleting file: {str(e)}" ) @router.get("/stats", response_model=Dict[str, Any]) async def getFileStats( currentUser: Dict[str, Any] = Depends(getCurrentActiveUser) ): """Returns statistics about the stored files""" try: context = await getContext(currentUser) # Get all files - metadata only allFiles = context.interfaceData.getAllFiles() # Calculate statistics totalFiles = len(allFiles) totalSize = sum(file.get("size", 0) for file in allFiles) # Group by file type fileTypes = {} for file in allFiles: fileType = file.get("mimeType", "unknown").split("/")[0] if fileType not in fileTypes: fileTypes[fileType] = 0 fileTypes[fileType] += 1 return { "totalFiles": totalFiles, "totalSizeBytes": totalSize, "fileTypes": fileTypes } except Exception as e: logger.error(f"Error retrieving file statistics: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error retrieving file statistics: {str(e)}" )