Spaces:
Running
Running
| from fastapi import APIRouter, HTTPException, Query | |
| from pydantic import BaseModel, Field, validator | |
| from typing import List, Optional | |
| from .utils import ( | |
| save_cameras, | |
| load_cameras, | |
| user_exists, | |
| _bucket_key, | |
| _list_prefix, | |
| _read_bucket_json, | |
| _write_bucket_json, | |
| BUCKET_ID, | |
| ) | |
| from huggingface_hub import batch_bucket_files, download_bucket_files | |
| import tempfile | |
| import os | |
| print(" CAMERA API LOADED ") | |
| router = APIRouter(prefix="/camera", tags=["Camera"]) | |
| # ================= MODELS ================= | |
| class CameraData(BaseModel): | |
| user_id: str = Field(..., min_length=1) | |
| camera_name: str = Field(..., min_length=1) | |
| camera_loc: Optional[List[float]] = None | |
| def validate_loc(cls, loc): | |
| if loc is None: | |
| return loc | |
| if len(loc) != 2: | |
| raise ValueError("camera_loc must be [lat, lon]") | |
| lat, lon = loc | |
| if not (-90 <= lat <= 90): | |
| raise ValueError("Latitude must be between -90 and 90") | |
| if not (-180 <= lon <= 180): | |
| raise ValueError("Longitude must be between -180 and 180") | |
| return loc | |
| class EditCameraData(BaseModel): | |
| user_id: str | |
| old_camera_name: str | |
| new_camera_name: str | |
| new_camera_loc: Optional[List[float]] = None # β optional, won't error if missing | |
| def validate_loc(cls, loc): | |
| if loc is None: | |
| return loc | |
| if len(loc) != 2: | |
| raise ValueError("new_camera_loc must be [lat, lon]") | |
| lat, lon = loc | |
| if not (-90 <= lat <= 90): | |
| raise ValueError("Latitude must be between -90 and 90") | |
| if not (-180 <= lon <= 180): | |
| raise ValueError("Longitude must be between -180 and 180") | |
| return loc | |
| # ================= ROUTES ================= | |
| def home(): | |
| return { | |
| "message": "Camera API is Running", | |
| "endpoints": [ | |
| "/camera/add_camera", | |
| "/camera/edit_camera", | |
| "/camera/delete_camera", | |
| "/camera/get_cameras?user_id=<id>" | |
| ] | |
| } | |
| # ---------- GET CAMERAS ---------- | |
| def get_cameras(user_id: str = Query(...)): | |
| if not user_exists(user_id): | |
| raise HTTPException(status_code=404, detail="User not found") | |
| cameras = load_cameras(user_id) | |
| return {"success": True, "user_id": user_id, "cameras": cameras, "count": len(cameras)} | |
| # ---------- ADD CAMERA ---------- | |
| def add_camera(data: CameraData): | |
| cameras = load_cameras(data.user_id) | |
| if len(cameras) >= 2: | |
| raise HTTPException(status_code=400, detail="Only 2 cameras allowed") | |
| for cam in cameras: | |
| if cam["camera_name"].lower() == data.camera_name.lower(): | |
| raise HTTPException(status_code=400, detail="Camera already exists") | |
| cameras.append({"camera_name": data.camera_name, "camera_loc": data.camera_loc}) | |
| save_cameras(data.user_id, cameras) | |
| return {"success": True, "camera": data.camera_name} | |
| # ---------- EDIT CAMERA ---------- | |
| def edit_camera(data: EditCameraData): | |
| if not user_exists(data.user_id): | |
| raise HTTPException(status_code=404, detail="User not found") | |
| cameras = load_cameras(data.user_id) | |
| # Only block duplicate name if name is actually changing | |
| name_changing = data.new_camera_name.lower() != data.old_camera_name.lower() | |
| if name_changing: | |
| if any(cam["camera_name"].lower() == data.new_camera_name.lower() for cam in cameras): | |
| raise HTTPException( | |
| status_code=400, | |
| detail=f"Camera name '{data.new_camera_name}' already exists" | |
| ) | |
| camera_found = False | |
| for cam in cameras: | |
| if cam["camera_name"].lower() == data.old_camera_name.lower(): | |
| old_name = cam["camera_name"] | |
| # Apply updates | |
| cam["camera_name"] = data.new_camera_name | |
| if data.new_camera_loc is not None: # only update if provided | |
| cam["camera_loc"] = data.new_camera_loc | |
| camera_found = True | |
| # ββ Rename bucket files only if name changed ββββββββββ | |
| if name_changing: | |
| old_prefix = _bucket_key(data.user_id, old_name) | |
| new_prefix = _bucket_key(data.user_id, data.new_camera_name) | |
| old_files = _list_prefix(old_prefix) | |
| for item in old_files: | |
| old_key = item.path | |
| new_key = old_key.replace(old_prefix, new_prefix, 1) | |
| # Rename detection JSON filename inside the key | |
| if f"{old_name}_detections.json" in new_key: | |
| new_key = new_key.replace( | |
| f"{old_name}_detections.json", | |
| f"{data.new_camera_name}_detections.json" | |
| ) | |
| try: | |
| if old_key.endswith(".json"): | |
| # JSON: read and re-write | |
| content = _read_bucket_json(old_key) | |
| if content is not None: | |
| _write_bucket_json(new_key, content) | |
| else: | |
| # Binary (images): download β re-upload | |
| with tempfile.NamedTemporaryFile(delete=False) as tf: | |
| tmp_path = tf.name | |
| download_bucket_files( | |
| BUCKET_ID, | |
| files=[(old_key, tmp_path)], | |
| ) | |
| with open(tmp_path, "rb") as f: | |
| raw = f.read() | |
| os.unlink(tmp_path) | |
| batch_bucket_files( | |
| BUCKET_ID, | |
| add=[(raw, new_key)], | |
| ) | |
| # Delete old key after successful copy | |
| batch_bucket_files( | |
| BUCKET_ID, | |
| delete=[old_key], | |
| ) | |
| except Exception as e: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Failed to move bucket file '{old_key}': {str(e)}" | |
| ) | |
| save_cameras(data.user_id, cameras) | |
| return {"success": True, "updated": cam} | |
| if not camera_found: | |
| raise HTTPException(status_code=404, detail="Camera not found") | |
| # ---------- DELETE CAMERA ---------- | |
| def delete_camera(user_id: str = Query(...), camera_name: str = Query(...)): | |
| if not user_exists(user_id): | |
| raise HTTPException(status_code=404, detail="User not found") | |
| cameras = load_cameras(user_id) | |
| new_list = [c for c in cameras if c["camera_name"].lower() != camera_name.lower()] | |
| if len(new_list) == len(cameras): | |
| raise HTTPException(status_code=404, detail="Camera not found") | |
| # Delete all bucket files under this camera prefix | |
| cam_prefix = _bucket_key(user_id, camera_name) | |
| cam_files = _list_prefix(cam_prefix) | |
| if cam_files: | |
| keys_to_delete = [item.path for item in cam_files] | |
| try: | |
| batch_bucket_files( | |
| BUCKET_ID, | |
| delete=keys_to_delete, | |
| ) | |
| except Exception as e: | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"Failed to delete camera files from bucket: {str(e)}" | |
| ) | |
| save_cameras(user_id, new_list) | |
| return {"success": True, "deleted": camera_name} |