gateway/routes/files.py
2025-04-21 20:04:22 +02:00

270 lines
No EOL
9 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.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)}"
)