gateway/modules/routes/routeFiles.py

245 lines
No EOL
8.4 KiB
Python

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.security.auth import getCurrentActiveUser, getUserContext
from modules.shared.configuration import APP_CONFIG
# Import interfaces
from modules.interfaces.lucydomInterface import getLucydomInterface, FileError, FileNotFoundError, FileStorageError, FilePermissionError, FileDeletionError
from modules.interfaces.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)
class AppContext:
def __init__(self, mandateId: int, userId: int):
self._mandateId = mandateId
self._userId = userId
self.interfaceData = getLucydomInterface(mandateId, userId)
async def getContext(currentUser: Dict[str, Any]) -> AppContext:
"""
Creates a central context object with all required interfaces
Args:
currentUser: Current user from authentication
Returns:
AppContext object with all required connections
"""
_mandateId, _userId = await getUserContext(currentUser)
return AppContext(_mandateId, _userId)
# 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)}"
)