from fastapi import APIRouter, HTTPException, Request from fastapi.responses import Response from app.services.backend_manager import get_backend router = APIRouter() def _validate_key(key: str) -> str: """Reject path traversal. Key may contain '/' for nested objects (e.g. user/doc.pdf).""" parts = key.split("/") if ".." in parts: raise HTTPException(status_code=400, detail="Invalid key: path traversal not allowed") return key @router.put("/objects/{bucket}/{key:path}", status_code=204) async def put_object(bucket: str, key: str, request: Request): """Upload raw bytes. Body is read as-is (application/octet-stream).""" _validate_key(key) data = await request.body() try: await get_backend().put(bucket, key, data) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) @router.get("/objects/{bucket}/{key:path}") async def get_object(bucket: str, key: str): """Download raw bytes.""" _validate_key(key) try: data = await get_backend().get(bucket, key) except KeyError: raise HTTPException(status_code=404, detail="Object not found") except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) return Response(content=data, media_type="application/octet-stream") @router.delete("/objects/{bucket}/{key:path}", status_code=204) async def delete_object(bucket: str, key: str): """Delete an object. No-op if it does not exist.""" _validate_key(key) try: await get_backend().delete(bucket, key) except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) @router.get("/objects/{bucket}") async def list_objects(bucket: str): """List all keys in a bucket.""" try: keys = await get_backend().list_keys(bucket) except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) return {"bucket": bucket, "keys": keys}