diff --git a/packetserver/http/routers/objects.py b/packetserver/http/routers/objects.py index 44b2ca7..d65ea20 100644 --- a/packetserver/http/routers/objects.py +++ b/packetserver/http/routers/objects.py @@ -7,8 +7,8 @@ import mimetypes import logging from traceback import format_exc import base64 - -from pydantic import BaseModel +import traceback +from pydantic import BaseModel, validator from packetserver.http.dependencies import get_current_http_user from packetserver.http.auth import HttpUser @@ -253,4 +253,97 @@ async def create_binary_object( private=new_object.private, created_at=new_object.created_at, modified_at=new_object.modified_at + ) + +class ObjectUpdate(BaseModel): + name: Optional[str] = None + private: Optional[bool] = None + data_text: Optional[str] = None # New: update to text content (forces binary=False) + data_base64: Optional[str] = None # New: update to binary content (forces binary=True) + + @validator('data_text', 'data_base64', pre=True, always=True) + def check_mutually_exclusive(cls, v, values, field): + other_field = 'data_base64' if field.name == 'data_text' else 'data_text' + if v is not None and values.get(other_field) is not None: + raise ValueError('data_text and data_base64 cannot be provided together') + return v + +@router.patch("/objects/{uuid}", response_model=ObjectSummary) +async def update_object( + uuid: UUID, + payload: ObjectUpdate, + db: DbDependency, + current_user: HttpUser = Depends(get_current_http_user) +): + username = current_user.username + + if all(v is None for v in [payload.name, payload.private, payload.data_text, payload.data_base64]): + raise HTTPException(status_code=400, detail="No updates provided") + + try: + with db.transaction() as conn: + root = conn.root() + obj = Object.get_object_by_uuid(uuid, root) + if not obj: + raise HTTPException(status_code=404, detail="Object not found") + + user = User.get_user_by_username(username, root) + if not user or user.uuid != obj.owner: + raise HTTPException(status_code=403, detail="Not authorized to modify this object") + + updated = False + + if payload.name is not None: + new_name = payload.name.strip() + if len(new_name) > 300: + raise HTTPException(status_code=400, detail="Object name too long (max 300 chars)") + if not new_name: + raise HTTPException(status_code=400, detail="Invalid object name") + obj.name = new_name + updated = True + + if payload.private is not None: + obj.private = payload.private + updated = True + + if payload.data_text is not None: + if not payload.data_text: + raise HTTPException(status_code=400, detail="Text content cannot be empty") + obj.data = payload.data_text # str → forces binary=False, calls touch() + updated = True + + if payload.data_base64 is not None: + try: + content = base64.b64decode(payload.data_base64, validate=True) + except Exception: + raise HTTPException(status_code=400, detail="Invalid base64 encoding") + if not content: + raise HTTPException(status_code=400, detail="Binary content cannot be empty") + obj.data = content # bytes → forces binary=True, calls touch() + updated = True + + if not updated: + raise HTTPException(status_code=400, detail="No valid updates applied") + + logging.info(f"User {username} updated object {uuid}") + + except HTTPException: + raise + except Exception as e: + logging.error(f"Object update failed for {username} on {uuid}: {e}\n{traceback.format_exc()}") + raise HTTPException(status_code=500, detail="Failed to update object") + + content_type, _ = mimetypes.guess_type(obj.name) + if content_type is None: + content_type = "application/octet-stream" if obj.binary else "text/plain" + + return ObjectSummary( + uuid=obj.uuid, + name=obj.name, + binary=obj.binary, + size=obj.size, + content_type=content_type, + private=obj.private, + created_at=obj.created_at, + modified_at=obj.modified_at ) \ No newline at end of file