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 # Configure logger logger = logging.getLogger(__name__) # Get all attributes of the model (except internal/special attributes) 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'] # Model attributes for FileItem file_attributes = get_model_attributes(FileItem) @dataclass class AppContext: """Context object for all required connections and user information""" mandate_id: int user_id: int interface_data: Any # LucyDOM Interface async def get_context(current_user: Dict[str, Any]) -> AppContext: """ Creates a central context object with all required interfaces Args: current_user: Current user from authentication Returns: AppContext object with all required connections """ 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 ) # 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 get_files(current_user: Dict[str, Any] = Depends(get_current_active_user)): """Get all available files""" try: context = await get_context(current_user) # Get all files generically - only metadata, no binary data files = context.interface_data.get_all_files() 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 upload_file( file: UploadFile = File(...), workflow_id: Optional[str] = Form(None), current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Upload a file """ try: context = await get_context(current_user) # Read file file_content = await file.read() # Check size limits 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"File too large. Maximum size: {APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB" ) # Save file via LucyDOM interface in the database file_meta = context.interface_data.save_uploaded_file(file_content, file.filename) # If workflow_id is provided, update the file information 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 # Successful response return file_meta 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("/{file_id}") async def get_file( file_id: str, current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Returns a file by its ID for download. Retrieves both metadata and binary data. """ try: context = await get_context(current_user) # Get file via LucyDOM interface from the database # Uses the download_file method, which now combines metadata and binary data file_data = context.interface_data.download_file(file_id) # Return file 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"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("/{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) ): """ Deletes a file by its ID from the database. Removes both metadata and binary data. """ try: context = await get_context(current_user) # Delete file via LucyDOM interface # The method now handles deleting from both tables (files and file_data) context.interface_data.delete_file(file_id) # 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 get_file_stats( current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Returns statistics about the stored files. """ try: context = await get_context(current_user) # Get all files - metadata only all_files = context.interface_data.get_all_files() # Calculate statistics total_files = len(all_files) total_size = sum(file.get("size", 0) for file in all_files) # Group by file type 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"Error retrieving file statistics: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error retrieving file statistics: {str(e)}" )