from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response, Body from fastapi.responses import JSONResponse, FileResponse from typing import List, Dict, Any, Optional import logging from datetime import datetime, timezone from dataclasses import dataclass import io # Import auth module from modules.security.auth import limiter, getCurrentUser # Import interfaces import modules.interfaces.serviceManagementClass as serviceManagementClass from modules.interfaces.serviceManagementModel import FileItem from modules.shared.attributeUtils import getModelAttributeDefinitions from modules.interfaces.serviceAppModel import AttributeDefinition, User # Configure logger logger = logging.getLogger(__name__) # Model attributes for FileItem fileAttributes = getModelAttributeDefinitions(FileItem) # Create router for file endpoints router = APIRouter( prefix="/api/files", tags=["Manage Files"], responses={ 404: {"description": "Not found"}, 400: {"description": "Bad request"}, 401: {"description": "Unauthorized"}, 403: {"description": "Forbidden"}, 500: {"description": "Internal server error"} } ) @router.get("/list", response_model=List[FileItem]) @limiter.limit("30/minute") async def get_files( request: Request, currentUser: User = Depends(getCurrentUser) ) -> List[FileItem]: """Get all files""" try: managementInterface = serviceManagementClass.getInterface(currentUser) # Get all files generically - only metadata, no binary data files = managementInterface.getAllFiles() return [FileItem(**file) for file in files] except Exception as e: logger.error(f"Error getting files: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to get files: {str(e)}" ) @router.post("/upload", status_code=status.HTTP_201_CREATED) @limiter.limit("10/minute") async def upload_file( request: Request, file: UploadFile = File(...), workflowId: Optional[str] = Form(None), currentUser: User = Depends(getCurrentUser) ) -> JSONResponse: """Upload a file""" try: managementInterface = serviceManagementClass.getInterface(currentUser) # Read file fileContent = await file.read() # Check size limits maxSize = int(serviceManagementClass.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: {serviceManagementClass.APP_CONFIG.get('File_Management_MAX_UPLOAD_SIZE_MB')}MB" ) # Save file via LucyDOM interface in the database fileMeta = managementInterface.saveUploadedFile(fileContent, file.filename) # If workflowId is provided, update the file information if workflowId: updateData = {"workflowId": workflowId} managementInterface.updateFile(fileMeta["id"], updateData) fileMeta["workflowId"] = workflowId # Successful response return JSONResponse({ "message": "File uploaded successfully", "file": fileMeta }) except serviceManagementClass.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}", response_model=FileItem) @limiter.limit("30/minute") async def get_file( request: Request, fileId: str = Path(..., description="ID of the file"), currentUser: User = Depends(getCurrentUser) ) -> FileItem: """Get a file""" try: managementInterface = serviceManagementClass.getInterface(currentUser) # Get file via LucyDOM interface from the database fileData = managementInterface.getFile(fileId) if not fileData: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"File with ID {fileId} not found" ) return FileItem(**fileData) except serviceManagementClass.FileNotFoundError as e: logger.warning(f"File not found: {str(e)}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) except serviceManagementClass.FilePermissionError as e: logger.warning(f"No permission for file: {str(e)}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail=str(e) ) except serviceManagementClass.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.put("/{fileId}", response_model=FileItem) @limiter.limit("10/minute") async def update_file( request: Request, fileId: str = Path(..., description="ID of the file to update"), file_info: Dict[str, Any] = Body(...), currentUser: User = Depends(getCurrentUser) ) -> FileItem: """Update file info""" try: managementInterface = serviceManagementClass.getInterface(currentUser) # Get the file from the database file = managementInterface.getFile(fileId) if not file: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"File with ID {fileId} not found" ) # Check if user has access to the file if file.get("userId", 0) != currentUser.get("id", 0): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized to update this file" ) # Update the file result = managementInterface.updateFile(fileId, file_info) if not result: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update file" ) # Get updated file and convert to FileItem updatedFile = managementInterface.getFile(fileId) return FileItem(**updatedFile) except HTTPException as he: raise he except Exception as e: logger.error(f"Error updating file: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) ) @router.delete("/{fileId}", status_code=status.HTTP_204_NO_CONTENT) @limiter.limit("10/minute") async def delete_file( request: Request, fileId: str, currentUser: User = Depends(getCurrentUser) ) -> JSONResponse: """Delete a file""" try: managementInterface = serviceManagementClass.getInterface(currentUser) # Delete file via LucyDOM interface managementInterface.deleteFile(fileId) # Return successful deletion without content (204 No Content) return JSONResponse({ "message": "File deleted successfully" }) except serviceManagementClass.FileNotFoundError as e: logger.warning(f"File not found: {str(e)}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=str(e) ) except serviceManagementClass.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 serviceManagementClass.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]) @limiter.limit("30/minute") async def get_file_stats( request: Request, currentUser: User = Depends(getCurrentUser) ) -> Dict[str, Any]: """Returns statistics about the stored files""" try: managementInterface = serviceManagementClass.getInterface(currentUser) # Get all files - metadata only allFiles = managementInterface.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)}" ) @router.get("/attributes", response_model=List[AttributeDefinition]) @limiter.limit("30/minute") async def get_file_attributes( request: Request, currentUser: User = Depends(getCurrentUser) ) -> List[AttributeDefinition]: """ Retrieves the attribute definitions for files. This can be used for dynamic form generation. Returns: - A list of attribute definitions that can be used to generate forms """ # Get attributes from the FileItem model class return FileItem.getModelAttributeDefinitions()