233 lines
7.3 KiB
Python
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"
|
|
}
|