diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 62b6954..c2528a0 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -51,6 +51,9 @@ class CelestialBodyUpdate(BaseModel): is_active: Optional[bool] = None extra_data: Optional[Dict[str, Any]] = None +class ResourceUpdate(BaseModel): + extra_data: Optional[Dict[str, Any]] = None + @router.post("/", status_code=status.HTTP_201_CREATED) async def create_celestial_body( @@ -782,13 +785,17 @@ async def get_static_data( @router.post("/resources/upload") async def upload_resource( - body_id: Optional[str] = None, + 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 """ import os import aiofiles @@ -802,15 +809,37 @@ async def upload_resource( 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") / resource_type + upload_dir = Path("upload") / upload_subdir upload_dir.mkdir(parents=True, exist_ok=True) - # Generate unique filename - import uuid - file_ext = os.path.splitext(file.filename)[1] - unique_filename = f"{uuid.uuid4()}{file_ext}" - file_path = upload_dir / unique_filename + # 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(): + from datetime import datetime + 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: @@ -821,29 +850,42 @@ async def upload_resource( # 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": str(file_path), + "file_path": relative_path, "file_size": file_size, - "mime_type": file.content_type, + "mime_type": mime_type, }, db ) - logger.info(f"Uploaded resource: {file_path} ({file_size} bytes)") + # 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, - "message": "File uploaded successfully" + "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) @@ -871,6 +913,7 @@ async def get_body_resources( "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} @@ -914,6 +957,47 @@ async def delete_resource( raise HTTPException(status_code=500, detail="Failed to delete resource") +@router.put("/resources/{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) + """ + from sqlalchemy import select, update + + # 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" + } + + # ============================================================ # Orbit Management APIs diff --git a/backend/app/models/celestial.py b/backend/app/models/celestial.py index 9b80d91..e6ca46c 100644 --- a/backend/app/models/celestial.py +++ b/backend/app/models/celestial.py @@ -21,7 +21,7 @@ class CelestialBody(BaseModel): id: str = Field(..., description="JPL Horizons ID") name: str = Field(..., description="Display name") name_zh: str | None = Field(None, description="Chinese name") - type: Literal["planet", "probe", "star", "dwarf_planet", "satellite"] = Field(..., description="Body type") + type: Literal["planet", "probe", "star", "dwarf_planet", "satellite", "comet"] = Field(..., description="Body type") positions: list[Position] = Field( default_factory=list, description="Position history" ) @@ -43,7 +43,7 @@ class BodyInfo(BaseModel): id: str name: str - type: Literal["planet", "probe", "star", "dwarf_planet", "satellite"] + type: Literal["planet", "probe", "star", "dwarf_planet", "satellite", "comet"] description: str launch_date: str | None = None status: str | None = None diff --git a/backend/upload/model/hubble_space_telescope.glb b/backend/upload/model/hubble_space_telescope.glb deleted file mode 100644 index 8ce60c6..0000000 Binary files a/backend/upload/model/hubble_space_telescope.glb and /dev/null differ diff --git a/backend/upload/texture/1I:Oumuamua.jpg b/backend/upload/texture/1I:Oumuamua.jpg new file mode 100644 index 0000000..cc4671c Binary files /dev/null and b/backend/upload/texture/1I:Oumuamua.jpg differ diff --git a/backend/upload/texture/1I:Oumuamua_20251130_230201.jpg b/backend/upload/texture/1I:Oumuamua_20251130_230201.jpg new file mode 100644 index 0000000..88db736 Binary files /dev/null and b/backend/upload/texture/1I:Oumuamua_20251130_230201.jpg differ diff --git a/backend/upload/texture/1P:Halley.jpg b/backend/upload/texture/1P:Halley.jpg new file mode 100644 index 0000000..a11147d Binary files /dev/null and b/backend/upload/texture/1P:Halley.jpg differ diff --git a/backend/upload/texture/3I:Atlas.jpg b/backend/upload/texture/3I:Atlas.jpg new file mode 100644 index 0000000..a11147d Binary files /dev/null and b/backend/upload/texture/3I:Atlas.jpg differ diff --git a/backend/upload/texture/3I:Atlas_20251130_224738.jpg b/backend/upload/texture/3I:Atlas_20251130_224738.jpg new file mode 100644 index 0000000..0da5a37 Binary files /dev/null and b/backend/upload/texture/3I:Atlas_20251130_224738.jpg differ diff --git a/backend/upload/texture/3I:Atlas_20251130_225754.jpg b/backend/upload/texture/3I:Atlas_20251130_225754.jpg new file mode 100644 index 0000000..d24ec64 Binary files /dev/null and b/backend/upload/texture/3I:Atlas_20251130_225754.jpg differ