From e69a5aa1bed1c30d6b4100507c6cf72299008e5e Mon Sep 17 00:00:00 2001 From: Jedrzej Kosinski Date: Fri, 16 Jan 2026 00:14:03 -0800 Subject: [PATCH] Finished @ROUTES.put(f"/api/assets/{{id:{UUID_RE}}}/preview") --- app/assets/api/routes.py | 26 ++++++++++++++++++++++ app/assets/database/queries.py | 22 +++++++++++++++++++ app/assets/manager.py | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/app/assets/api/routes.py b/app/assets/api/routes.py index 8f40c8b65..76ebf0d98 100644 --- a/app/assets/api/routes.py +++ b/app/assets/api/routes.py @@ -380,3 +380,29 @@ async def update_asset(request: web.Request) -> web.Response: return _error_response(500, "INTERNAL", "Unexpected server error.") return web.json_response(result.model_dump(mode="json"), status=200) +@ROUTES.put(f"/api/assets/{{id:{UUID_RE}}}/preview") +async def set_asset_preview(request: web.Request) -> web.Response: + asset_info_id = str(uuid.UUID(request.match_info["id"])) + try: + body = schemas_in.SetPreviewBody.model_validate(await request.json()) + except ValidationError as ve: + return _validation_error_response("INVALID_BODY", ve) + except Exception: + return _error_response(400, "INVALID_JSON", "Request body must be valid JSON.") + + try: + result = manager.set_asset_preview( + asset_info_id=asset_info_id, + preview_asset_id=body.preview_id, + owner_id=USER_MANAGER.get_request_user_id(request), + ) + except (PermissionError, ValueError) as ve: + return _error_response(404, "ASSET_NOT_FOUND", str(ve), {"id": asset_info_id}) + except Exception: + logging.exception( + "set_asset_preview failed for asset_info_id=%s, owner_id=%s", + asset_info_id, + USER_MANAGER.get_request_user_id(request), + ) + return _error_response(500, "INTERNAL", "Unexpected server error.") + return web.json_response(result.model_dump(mode="json"), status=200) diff --git a/app/assets/database/queries.py b/app/assets/database/queries.py index d23143485..8a6905783 100644 --- a/app/assets/database/queries.py +++ b/app/assets/database/queries.py @@ -810,3 +810,25 @@ def remove_missing_tag_for_asset_id( AssetInfoTag.tag_name == "missing", ) ) + +def set_asset_info_preview( + session: Session, + *, + asset_info_id: str, + preview_asset_id: str | None = None, +) -> None: + """Set or clear preview_id and bump updated_at. Raises on unknown IDs.""" + info = session.get(AssetInfo, asset_info_id) + if not info: + raise ValueError(f"AssetInfo {asset_info_id} not found") + + if preview_asset_id is None: + info.preview_id = None + else: + # validate preview asset exists + if not session.get(Asset, preview_asset_id): + raise ValueError(f"Preview Asset {preview_asset_id} not found") + info.preview_id = preview_asset_id + + info.updated_at = utcnow() + session.flush() diff --git a/app/assets/manager.py b/app/assets/manager.py index 0c64a1fd8..62e787477 100644 --- a/app/assets/manager.py +++ b/app/assets/manager.py @@ -20,6 +20,7 @@ from app.assets.database.queries import ( get_asset_tags, pick_best_live_path, ingest_fs_asset, + set_asset_info_preview, ) from app.assets.helpers import resolve_destination_from_tags, ensure_within_base @@ -318,6 +319,45 @@ def update_asset( ) +def set_asset_preview( + *, + asset_info_id: str, + preview_asset_id: str | None = None, + owner_id: str = "", +) -> schemas_out.AssetDetail: + with create_session() as session: + info_row = get_asset_info_by_id(session, asset_info_id=asset_info_id) + if not info_row: + raise ValueError(f"AssetInfo {asset_info_id} not found") + if info_row.owner_id and info_row.owner_id != owner_id: + raise PermissionError("not owner") + + set_asset_info_preview( + session, + asset_info_id=asset_info_id, + preview_asset_id=preview_asset_id, + ) + + res = fetch_asset_info_asset_and_tags(session, asset_info_id=asset_info_id, owner_id=owner_id) + if not res: + raise RuntimeError("State changed during preview update") + info, asset, tags = res + session.commit() + + return schemas_out.AssetDetail( + id=info.id, + name=info.name, + asset_hash=asset.hash if asset else None, + size=int(asset.size_bytes) if asset and asset.size_bytes is not None else None, + mime_type=asset.mime_type if asset else None, + tags=tags, + user_metadata=info.user_metadata or {}, + preview_id=info.preview_id, + created_at=info.created_at, + last_access_time=info.last_access_time, + ) + + def create_asset_from_hash( *, hash_str: str,