cosmo/backend/app/api/celestial_resource.py

233 lines
7.3 KiB
Python

"""
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"
}