from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, BackgroundTasks, Request from fastapi.responses import StreamingResponse from typing import Optional, List from io import BytesIO import logging from datetime import datetime import os import json from pydantic import BaseModel from app.schemas.file import ( FileSchema, CreateFileRequest, CreateFileResponse, GetFileResponse, DeleteFileResponse, GetFileQueryParams ) from app.schemas.box import ( CreateBoxResponse, CreateBoxRequest, BoxSchema, UpdateBoxRequest, CreateOpenBoxRequest, CreateOpenBoxResponse, OpenBoxSchema ) from app.services.file import FileService from app.services.box import BoxService from app.services.task import TaskService from app.services.pricing import PricingService from app.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, get_pricing_service ) 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, success = box_service.create_box(box_data) if not success: raise HTTPException(status_code=400, detail="Box creation failed, box already exists") 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)) @router.delete("/boxes/{box_id}/open", response_model=CreateOpenBoxResponse, status_code=200) async def delete_open_box( box_id: str, box_service: BoxService = Depends(get_box_service) ) -> CreateOpenBoxResponse: """Delete an open box by ID.""" try: result = box_service.delete_open_box(box_id) return CreateOpenBoxResponse( status_code=201, success=True, open_box=[OpenBoxSchema.from_orm(result)] ) except Exception as e: logger.error(f"Delete open box failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e) ) class InventoryAddRequest(BaseModel): open_box_ids: List[str] @router.post("/tcgplayer/inventory/add", response_class=StreamingResponse) async def create_inventory_add_file( body: InventoryAddRequest, pricing_service: PricingService = Depends(get_pricing_service), ): """Create a new inventory add file for download.""" try: content = pricing_service.generate_tcgplayer_inventory_update_file_with_pricing(body.open_box_ids) stream = BytesIO(content) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") return StreamingResponse( iter([stream.getvalue()]), media_type="text/csv", headers={ 'Content-Disposition': f'attachment; filename="inventory_add_{timestamp}.csv"' } ) except Exception as e: logger.error(f"Create inventory add file failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) @router.get("/tcgplayer/inventory/update", response_class=StreamingResponse) async def create_inventory_update_file( pricing_service: PricingService = Depends(get_pricing_service), ): """Create a new inventory update file for download.""" try: content = pricing_service.generate_tcgplayer_inventory_update_file_with_pricing() stream = BytesIO(content) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") return StreamingResponse( iter([stream.getvalue()]), media_type="text/csv", headers={ 'Content-Disposition': f'attachment; filename="inventory_update_{timestamp}.csv"' } ) except Exception as e: logger.error(f"Create inventory update file failed: {str(e)}") raise HTTPException(status_code=400, detail=str(e)) class CookieUpdate(BaseModel): cookies: dict # cookies @router.post("/cookies", response_model=dict) async def update_cookies( cookie_data: CookieUpdate ): try: # see if cookie file exists if not os.path.exists('cookies') or os.path.exists('cookies/tcg_cookies.json'): logger.info("Cannot find cookies") # Create cookies directory if it doesn't exist os.makedirs('cookies', exist_ok=True) # Save cookies with timestamp timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") cookie_path = 'cookies/tcg_cookies.json' # Save new cookies with open(cookie_path, 'w') as f: json.dump(cookie_data.cookies, f, indent=2) return {"message": "Cookies updated successfully"} except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to update cookies: {str(e)}" )