""" Resource Management API routes Handles file uploads and management for celestial body resources (textures, models, icons, etc.) """ import os import logging import aiofiles from pathlib import Path from datetime import datetime from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update from pydantic import BaseModel from typing import Optional, Dict, Any from app.database import get_db from app.models.db import Resource from app.services.db_service import celestial_body_service, resource_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/celestial/resources", tags=["celestial-resource"]) # Pydantic models class ResourceUpdate(BaseModel): extra_data: Optional[Dict[str, Any]] = None @router.post("/upload") async def upload_resource( body_id: str = Query(..., description="Celestial body ID"), resource_type: str = Query(..., description="Type: texture, model, icon, thumbnail, data"), file: UploadFile = File(...), db: AsyncSession = Depends(get_db) ): """ Upload a resource file (texture, model, icon, etc.) Upload directory logic: - Probes (type='probe'): upload to 'model' directory - Others (planet, satellite, etc.): upload to 'texture' directory """ # Validate resource type valid_types = ["texture", "model", "icon", "thumbnail", "data"] if resource_type not in valid_types: raise HTTPException( status_code=400, detail=f"Invalid resource_type. Must be one of: {valid_types}" ) # Get celestial body to determine upload directory body = await celestial_body_service.get_body_by_id(body_id, db) if not body: raise HTTPException(status_code=404, detail=f"Celestial body {body_id} not found") # Determine upload directory based on body type # Probes -> model directory, Others -> texture directory if body.type == 'probe' and resource_type in ['model', 'texture']: upload_subdir = 'model' elif resource_type in ['model', 'texture']: upload_subdir = 'texture' else: # For icon, thumbnail, data, use resource_type as directory upload_subdir = resource_type # Create upload directory structure upload_dir = Path("upload") / upload_subdir upload_dir.mkdir(parents=True, exist_ok=True) # Use original filename original_filename = file.filename file_path = upload_dir / original_filename # If file already exists, append timestamp to make it unique if file_path.exists(): name_without_ext = os.path.splitext(original_filename)[0] file_ext = os.path.splitext(original_filename)[1] timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") original_filename = f"{name_without_ext}_{timestamp}{file_ext}" file_path = upload_dir / original_filename # Save file try: async with aiofiles.open(file_path, 'wb') as f: content = await file.read() await f.write(content) # Get file size file_size = os.path.getsize(file_path) # Store relative path (from upload directory) relative_path = f"{upload_subdir}/{original_filename}" # Determine MIME type mime_type = file.content_type # Create resource record resource = await resource_service.create_resource( { "body_id": body_id, "resource_type": resource_type, "file_path": relative_path, "file_size": file_size, "mime_type": mime_type, }, db ) # Commit the transaction await db.commit() await db.refresh(resource) logger.info(f"Uploaded resource for {body.name} ({body.type}): {relative_path} ({file_size} bytes)") return { "id": resource.id, "resource_type": resource.resource_type, "file_path": resource.file_path, "file_size": resource.file_size, "upload_directory": upload_subdir, "message": f"File uploaded successfully to {upload_subdir} directory" } except Exception as e: # Rollback transaction await db.rollback() # Clean up file if database operation fails if file_path.exists(): os.remove(file_path) logger.error(f"Error uploading file: {e}") raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}") @router.get("/{body_id}") async def get_body_resources( body_id: str, resource_type: Optional[str] = Query(None, description="Filter by resource type"), db: AsyncSession = Depends(get_db) ): """ Get all resources associated with a celestial body """ resources = await resource_service.get_resources_by_body(body_id, resource_type, db) result = [] for resource in resources: result.append({ "id": resource.id, "resource_type": resource.resource_type, "file_path": resource.file_path, "file_size": resource.file_size, "mime_type": resource.mime_type, "created_at": resource.created_at.isoformat(), "extra_data": resource.extra_data, }) return {"body_id": body_id, "resources": result} @router.delete("/{resource_id}") async def delete_resource( resource_id: int, db: AsyncSession = Depends(get_db) ): """ Delete a resource file and its database record """ # Get resource record result = await db.execute( select(Resource).where(Resource.id == resource_id) ) resource = result.scalar_one_or_none() if not resource: raise HTTPException(status_code=404, detail="Resource not found") # Delete file if it exists file_path = resource.file_path if os.path.exists(file_path): try: os.remove(file_path) logger.info(f"Deleted file: {file_path}") except Exception as e: logger.warning(f"Failed to delete file {file_path}: {e}") # Delete database record deleted = await resource_service.delete_resource(resource_id, db) if deleted: return {"message": "Resource deleted successfully"} else: raise HTTPException(status_code=500, detail="Failed to delete resource") @router.put("/{resource_id}") async def update_resource( resource_id: int, update_data: ResourceUpdate, db: AsyncSession = Depends(get_db) ): """ Update resource metadata (e.g., scale parameter for models) """ # Get resource record result = await db.execute( select(Resource).where(Resource.id == resource_id) ) resource = result.scalar_one_or_none() if not resource: raise HTTPException(status_code=404, detail="Resource not found") # Update extra_data await db.execute( update(Resource) .where(Resource.id == resource_id) .values(extra_data=update_data.extra_data) ) await db.commit() # Get updated resource result = await db.execute( select(Resource).where(Resource.id == resource_id) ) updated_resource = result.scalar_one_or_none() return { "id": updated_resource.id, "extra_data": updated_resource.extra_data, "message": "Resource updated successfully" }