From fa51437eede107a73405201ab4d490607f8e5b80 Mon Sep 17 00:00:00 2001 From: Luke Mino-Altherr Date: Thu, 12 Mar 2026 16:29:51 -0700 Subject: [PATCH] =?UTF-8?q?Reject=20client-provided=20id,=20fix=20preview?= =?UTF-8?q?=20URLs,=20rename=20tags=E2=86=92total=5Ftags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reject 'id' field in multipart upload with 400 UNSUPPORTED_FIELD instead of silently ignoring it - Build preview URL from the preview asset's own metadata rather than the parent asset's - Rename 'tags' to 'total_tags' in TagsAdd/TagsRemove response schemas for clarity Co-Authored-By: Claude Opus 4.6 --- app/assets/api/routes.py | 14 ++++++++++---- app/assets/api/schemas_in.py | 1 - app/assets/api/schemas_out.py | 4 ++-- app/assets/api/upload.py | 8 +++++--- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/assets/api/routes.py b/app/assets/api/routes.py index e011f9a83..46a9af698 100644 --- a/app/assets/api/routes.py +++ b/app/assets/api/routes.py @@ -152,7 +152,14 @@ def _build_preview_url_from_view(tags: list[str], user_metadata: dict[str, Any] def _build_asset_response(result: schemas.AssetDetailResult | schemas.UploadResult) -> schemas_out.Asset: """Build an Asset response from a service result.""" - preview_url = _build_preview_url_from_view(result.tags, result.ref.user_metadata) + if result.ref.preview_id: + preview_detail = get_asset_detail(result.ref.preview_id) + if preview_detail: + preview_url = _build_preview_url_from_view(preview_detail.tags, preview_detail.ref.user_metadata) + else: + preview_url = None + else: + preview_url = _build_preview_url_from_view(result.tags, result.ref.user_metadata) return schemas_out.Asset( id=result.ref.id, name=result.ref.name, @@ -382,7 +389,6 @@ async def upload_asset(request: web.Request) -> web.Response: "name": parsed.provided_name, "user_metadata": parsed.user_metadata_raw, "hash": parsed.provided_hash, - "id": parsed.provided_id, "mime_type": parsed.provided_mime_type, "preview_id": parsed.provided_preview_id, } @@ -605,7 +611,7 @@ async def add_asset_tags(request: web.Request) -> web.Response: payload = schemas_out.TagsAdd( added=result.added, already_present=result.already_present, - tags=result.total_tags, + total_tags=result.total_tags, ) except PermissionError as pe: return _build_error_response(403, "FORBIDDEN", str(pe), {"id": reference_id}) @@ -652,7 +658,7 @@ async def delete_asset_tags(request: web.Request) -> web.Response: payload = schemas_out.TagsRemove( removed=result.removed, not_present=result.not_present, - tags=result.total_tags, + total_tags=result.total_tags, ) except PermissionError as pe: return _build_error_response(403, "FORBIDDEN", str(pe), {"id": reference_id}) diff --git a/app/assets/api/schemas_in.py b/app/assets/api/schemas_in.py index 535b31bf0..efb9c5439 100644 --- a/app/assets/api/schemas_in.py +++ b/app/assets/api/schemas_in.py @@ -45,7 +45,6 @@ class ParsedUpload: user_metadata_raw: str | None provided_hash: str | None provided_hash_exists: bool | None - provided_id: str | None = None provided_mime_type: str | None = None provided_preview_id: str | None = None diff --git a/app/assets/api/schemas_out.py b/app/assets/api/schemas_out.py index 972d88022..8db642b45 100644 --- a/app/assets/api/schemas_out.py +++ b/app/assets/api/schemas_out.py @@ -54,14 +54,14 @@ class TagsAdd(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) added: list[str] = Field(default_factory=list) already_present: list[str] = Field(default_factory=list) - tags: list[str] = Field(default_factory=list) + total_tags: list[str] = Field(default_factory=list) class TagsRemove(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) removed: list[str] = Field(default_factory=list) not_present: list[str] = Field(default_factory=list) - tags: list[str] = Field(default_factory=list) + total_tags: list[str] = Field(default_factory=list) class TagHistogram(BaseModel): diff --git a/app/assets/api/upload.py b/app/assets/api/upload.py index c36257ae0..13d3d372c 100644 --- a/app/assets/api/upload.py +++ b/app/assets/api/upload.py @@ -52,7 +52,6 @@ async def parse_multipart_upload( user_metadata_raw: str | None = None provided_hash: str | None = None provided_hash_exists: bool | None = None - provided_id: str | None = None provided_mime_type: str | None = None provided_preview_id: str | None = None @@ -132,7 +131,11 @@ async def parse_multipart_upload( elif fname == "user_metadata": user_metadata_raw = (await field.text()) or None elif fname == "id": - provided_id = ((await field.text()) or "").strip() or None + raise UploadError( + 400, + "UNSUPPORTED_FIELD", + "Client-provided 'id' is not supported. Asset IDs are assigned by the server.", + ) elif fname == "mime_type": provided_mime_type = ((await field.text()) or "").strip() or None elif fname == "preview_id": @@ -161,7 +164,6 @@ async def parse_multipart_upload( user_metadata_raw=user_metadata_raw, provided_hash=provided_hash, provided_hash_exists=provided_hash_exists, - provided_id=provided_id, provided_mime_type=provided_mime_type, provided_preview_id=provided_preview_id, )