from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, BackgroundTasks from fastapi.responses import StreamingResponse from typing import Optional import logging from schemas.file import ( FileSchema, CreateFileRequest, CreateFileResponse, GetFileResponse, DeleteFileResponse, GetFileQueryParams ) from schemas.box import ( CreateBoxResponse, CreateBoxRequest, BoxSchema, UpdateBoxRequest, CreateOpenBoxRequest, CreateOpenBoxResponse, OpenBoxSchema ) from services.file import FileService from services.box import BoxService from services.task import TaskService from dependencies import ( get_file_service, get_box_service, get_task_service, get_create_file_metadata, get_box_data, get_box_update_data, get_open_box_data ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/api", tags=["cards"]) MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB async def validate_file_upload(file: UploadFile) -> bytes: """Validate uploaded file and return its contents.""" if not file.filename: raise HTTPException(status_code=400, detail="No filename provided") content = await file.read() if len(content) > MAX_FILE_SIZE: raise HTTPException(status_code=413, detail="File too large") return content @router.post("/files", response_model=CreateFileResponse, status_code=201) async def create_file( background_tasks: BackgroundTasks, file: UploadFile = File(...), metadata: CreateFileRequest = Depends(get_create_file_metadata), file_service: FileService = Depends(get_file_service), task_service: TaskService = Depends(get_task_service) ) -> CreateFileResponse: """Create a new file entry with the uploaded file.""" try: content = await validate_file_upload(file) logger.debug(f"File received: {file.filename}") logger.debug(f"Metadata: {metadata}") metadata.filename = metadata.filename or file.filename if not file_service.validate_file(content, metadata): raise HTTPException(status_code=400, detail="Invalid file content") created_file = file_service.create_file(content, metadata) if metadata.source == 'manabox': background_tasks.add_task(task_service.process_manabox_file, created_file) return CreateFileResponse( status_code=201, success=True, files=[FileSchema.from_orm(created_file)] ) except HTTPException as http_ex: raise http_ex except Exception as e: logger.error(f"File upload failed: {str(e)}") raise HTTPException( status_code=500, detail="Internal server error occurred during file upload" ) finally: await file.close() @router.get("/files/{file_id:path}", response_model=GetFileResponse) @router.get("/files", response_model=GetFileResponse) async def get_file( file_id: Optional[str] = None, query: GetFileQueryParams = Depends(), file_service: FileService = Depends(get_file_service) ) -> GetFileResponse: """ Get file(s) by optional ID and/or status. If file_id is provided, returns that specific file. If status is provided, returns all files with that status. If neither is provided, returns all files. """ try: if file_id: file = file_service.get_file(file_id) files = [file] else: files = file_service.get_files(status=query.status) return GetFileResponse( status_code=200, success=True, files=[FileSchema.from_orm(f) for f in files] ) except Exception as e: logger.error(f"Get file(s) failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.delete("/files/{file_id}", response_model=DeleteFileResponse) async def delete_file( file_id: str, file_service: FileService = Depends(get_file_service) ) -> DeleteFileResponse: """Delete a file by ID.""" try: file = file_service.delete_file(file_id) return DeleteFileResponse( status_code=200, success=True, files=[FileSchema.from_orm(file)] ) except Exception as e: logger.error(f"Delete file failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.post("/boxes", response_model=CreateBoxResponse, status_code=201) async def create_box( box_data: CreateBoxRequest = Depends(get_box_data), box_service: BoxService = Depends(get_box_service) ) -> CreateBoxResponse: """Create a new box.""" try: result = box_service.create_box(box_data) return CreateBoxResponse( status_code=201, success=True, box=[BoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Create box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.put("/boxes/{box_id}", response_model=CreateBoxResponse) async def update_box( box_id: str, box_data: UpdateBoxRequest = Depends(get_box_update_data), box_service: BoxService = Depends(get_box_service) ) -> CreateBoxResponse: """Update an existing box.""" try: result = box_service.update_box(box_id, box_data) return CreateBoxResponse( status_code=200, success=True, box=[BoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Update box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.delete("/boxes/{box_id}", response_model=CreateBoxResponse) async def delete_box( box_id: str, box_service: BoxService = Depends(get_box_service) ) -> CreateBoxResponse: """Delete a box by ID.""" try: result = box_service.delete_box(box_id) return CreateBoxResponse( status_code=200, success=True, box=[BoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Delete box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.post("/boxes/{box_id}/open", response_model=CreateOpenBoxResponse, status_code=201) async def open_box( box_id: str, box_data: CreateOpenBoxRequest = Depends(get_open_box_data), box_service: BoxService = Depends(get_box_service) ) -> CreateOpenBoxResponse: """Open a box by ID.""" try: result = box_service.open_box(box_id, box_data) return CreateOpenBoxResponse( status_code=201, success=True, open_box=[OpenBoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Open box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e))