mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-05-10 01:02:56 +08:00
Add asset preview management endpoints (PUT/DELETE /api/assets/{id}/preview)
- PUT /api/assets/{id}/preview: sets the preview asset for an existing asset,
returns the full updated Asset object. Returns 404 if asset ID doesn't exist.
- DELETE /api/assets/{id}/preview: clears the preview asset link, returns 204.
Returns 404 if asset ID doesn't exist.
Added OpenAPI spec entries under the assets tag with no x-runtime restriction.
Implemented handlers using the existing set_asset_preview service function.
Added integration tests covering happy paths and 404 cases.
Co-authored-by: Matt Miller <MillerMedia@users.noreply.github.com>
This commit is contained in:
parent
65045730a6
commit
9bd5710acb
@ -36,6 +36,7 @@ from app.assets.services import (
|
||||
list_tags,
|
||||
remove_tags,
|
||||
resolve_asset_for_download,
|
||||
set_asset_preview,
|
||||
update_asset_metadata,
|
||||
upload_from_temp_path,
|
||||
)
|
||||
@ -545,6 +546,70 @@ async def delete_asset_route(request: web.Request) -> web.Response:
|
||||
return web.Response(status=204)
|
||||
|
||||
|
||||
@ROUTES.put(f"/api/assets/{{id:{UUID_RE}}}/preview")
|
||||
@_require_assets_feature_enabled
|
||||
async def set_asset_preview_route(request: web.Request) -> web.Response:
|
||||
reference_id = str(uuid.UUID(request.match_info["id"]))
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
return _build_error_response(
|
||||
400, "INVALID_JSON", "Request body must be valid JSON."
|
||||
)
|
||||
|
||||
preview_id = payload.get("preview_id")
|
||||
if not preview_id:
|
||||
return _build_error_response(
|
||||
400, "INVALID_BODY", "preview_id is required."
|
||||
)
|
||||
|
||||
try:
|
||||
result = set_asset_preview(
|
||||
reference_id=reference_id,
|
||||
preview_reference_id=preview_id,
|
||||
owner_id=USER_MANAGER.get_request_user_id(request),
|
||||
)
|
||||
response_payload = _build_asset_response(result)
|
||||
except PermissionError as pe:
|
||||
return _build_error_response(403, "FORBIDDEN", str(pe), {"id": reference_id})
|
||||
except ValueError as ve:
|
||||
return _build_error_response(
|
||||
404, "ASSET_NOT_FOUND", str(ve), {"id": reference_id}
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"set_asset_preview failed for reference_id=%s",
|
||||
reference_id,
|
||||
)
|
||||
return _build_error_response(500, "INTERNAL", "Unexpected server error.")
|
||||
return web.json_response(response_payload.model_dump(mode="json", exclude_none=True), status=200)
|
||||
|
||||
|
||||
@ROUTES.delete(f"/api/assets/{{id:{UUID_RE}}}/preview")
|
||||
@_require_assets_feature_enabled
|
||||
async def clear_asset_preview_route(request: web.Request) -> web.Response:
|
||||
reference_id = str(uuid.UUID(request.match_info["id"]))
|
||||
try:
|
||||
result = set_asset_preview(
|
||||
reference_id=reference_id,
|
||||
preview_reference_id=None,
|
||||
owner_id=USER_MANAGER.get_request_user_id(request),
|
||||
)
|
||||
except PermissionError as pe:
|
||||
return _build_error_response(403, "FORBIDDEN", str(pe), {"id": reference_id})
|
||||
except ValueError as ve:
|
||||
return _build_error_response(
|
||||
404, "ASSET_NOT_FOUND", str(ve), {"id": reference_id}
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"clear_asset_preview failed for reference_id=%s",
|
||||
reference_id,
|
||||
)
|
||||
return _build_error_response(500, "INTERNAL", "Unexpected server error.")
|
||||
return web.Response(status=204)
|
||||
|
||||
|
||||
@ROUTES.get("/api/tags")
|
||||
@_require_assets_feature_enabled
|
||||
async def get_tags(request: web.Request) -> web.Response:
|
||||
|
||||
57
openapi.yaml
57
openapi.yaml
@ -1821,6 +1821,63 @@ paths:
|
||||
"404":
|
||||
description: Asset not found
|
||||
|
||||
/api/assets/{id}/preview:
|
||||
put:
|
||||
operationId: setAssetPreview
|
||||
tags: [assets]
|
||||
summary: Set preview asset
|
||||
description: Sets the preview asset for an existing asset. The preview_id must reference an existing asset.
|
||||
x-feature-gate: enable-assets
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: The asset ID.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- preview_id
|
||||
properties:
|
||||
preview_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of the asset to use as the preview
|
||||
responses:
|
||||
"200":
|
||||
description: Preview set successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Asset"
|
||||
"404":
|
||||
description: Asset not found
|
||||
delete:
|
||||
operationId: clearAssetPreview
|
||||
tags: [assets]
|
||||
summary: Clear preview asset
|
||||
description: Clears the preview asset link for an existing asset.
|
||||
x-feature-gate: enable-assets
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: The asset ID.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
"204":
|
||||
description: Preview cleared
|
||||
"404":
|
||||
description: Asset not found
|
||||
|
||||
/api/assets/{id}/tags:
|
||||
post:
|
||||
operationId: addAssetTags
|
||||
|
||||
111
tests-unit/assets_test/test_preview.py
Normal file
111
tests-unit/assets_test/test_preview.py
Normal file
@ -0,0 +1,111 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
def test_set_preview_success(
|
||||
http: requests.Session, api_base: str, asset_factory, make_asset_bytes
|
||||
):
|
||||
"""PUT /api/assets/{id}/preview sets the preview and returns the full Asset."""
|
||||
main_data = make_asset_bytes("main_asset.png", 2048)
|
||||
preview_data = make_asset_bytes("preview_asset.png", 1024)
|
||||
|
||||
main = asset_factory("main_asset.png", ["input", "unit-tests"], {}, main_data)
|
||||
preview = asset_factory("preview_asset.png", ["input", "unit-tests"], {}, preview_data)
|
||||
|
||||
r = http.put(
|
||||
f"{api_base}/api/assets/{main['id']}/preview",
|
||||
json={"preview_id": preview["id"]},
|
||||
timeout=120,
|
||||
)
|
||||
body = r.json()
|
||||
assert r.status_code == 200, body
|
||||
assert body["id"] == main["id"]
|
||||
assert body["preview_id"] == preview["id"]
|
||||
|
||||
|
||||
def test_clear_preview_success(
|
||||
http: requests.Session, api_base: str, asset_factory, make_asset_bytes
|
||||
):
|
||||
"""DELETE /api/assets/{id}/preview clears the preview and returns 204."""
|
||||
main_data = make_asset_bytes("main_clear.png", 2048)
|
||||
preview_data = make_asset_bytes("prev_clear.png", 1024)
|
||||
|
||||
main = asset_factory("main_clear.png", ["input", "unit-tests"], {}, main_data)
|
||||
preview = asset_factory("prev_clear.png", ["input", "unit-tests"], {}, preview_data)
|
||||
|
||||
# First set a preview
|
||||
r_set = http.put(
|
||||
f"{api_base}/api/assets/{main['id']}/preview",
|
||||
json={"preview_id": preview["id"]},
|
||||
timeout=120,
|
||||
)
|
||||
assert r_set.status_code == 200
|
||||
|
||||
# Now clear it
|
||||
r_del = http.delete(f"{api_base}/api/assets/{main['id']}/preview", timeout=120)
|
||||
assert r_del.status_code == 204
|
||||
|
||||
# Verify preview_id is cleared
|
||||
r_get = http.get(f"{api_base}/api/assets/{main['id']}", timeout=120)
|
||||
detail = r_get.json()
|
||||
assert r_get.status_code == 200, detail
|
||||
assert detail.get("preview_id") is None
|
||||
|
||||
|
||||
def test_set_preview_asset_not_found(http: requests.Session, api_base: str):
|
||||
"""PUT /api/assets/{id}/preview returns 404 for non-existent asset ID."""
|
||||
fake_id = str(uuid.uuid4())
|
||||
r = http.put(
|
||||
f"{api_base}/api/assets/{fake_id}/preview",
|
||||
json={"preview_id": str(uuid.uuid4())},
|
||||
timeout=120,
|
||||
)
|
||||
body = r.json()
|
||||
assert r.status_code == 404
|
||||
assert body["error"]["code"] == "ASSET_NOT_FOUND"
|
||||
|
||||
|
||||
def test_clear_preview_asset_not_found(http: requests.Session, api_base: str):
|
||||
"""DELETE /api/assets/{id}/preview returns 404 for non-existent asset ID."""
|
||||
fake_id = str(uuid.uuid4())
|
||||
r = http.delete(f"{api_base}/api/assets/{fake_id}/preview", timeout=120)
|
||||
body = r.json()
|
||||
assert r.status_code == 404
|
||||
assert body["error"]["code"] == "ASSET_NOT_FOUND"
|
||||
|
||||
|
||||
def test_set_preview_missing_body(
|
||||
http: requests.Session, api_base: str, asset_factory, make_asset_bytes
|
||||
):
|
||||
"""PUT /api/assets/{id}/preview returns 400 when preview_id is not provided."""
|
||||
main_data = make_asset_bytes("main_nobody.png", 2048)
|
||||
main = asset_factory("main_nobody.png", ["input", "unit-tests"], {}, main_data)
|
||||
|
||||
r = http.put(
|
||||
f"{api_base}/api/assets/{main['id']}/preview",
|
||||
json={},
|
||||
timeout=120,
|
||||
)
|
||||
body = r.json()
|
||||
assert r.status_code == 400
|
||||
assert body["error"]["code"] == "INVALID_BODY"
|
||||
|
||||
|
||||
def test_set_preview_invalid_preview_id(
|
||||
http: requests.Session, api_base: str, asset_factory, make_asset_bytes
|
||||
):
|
||||
"""PUT /api/assets/{id}/preview returns 404 when preview_id references non-existent asset."""
|
||||
main_data = make_asset_bytes("main_badprev.png", 2048)
|
||||
main = asset_factory("main_badprev.png", ["input", "unit-tests"], {}, main_data)
|
||||
|
||||
fake_preview_id = str(uuid.uuid4())
|
||||
r = http.put(
|
||||
f"{api_base}/api/assets/{main['id']}/preview",
|
||||
json={"preview_id": fake_preview_id},
|
||||
timeout=120,
|
||||
)
|
||||
body = r.json()
|
||||
assert r.status_code == 404
|
||||
assert body["error"]["code"] == "ASSET_NOT_FOUND"
|
||||
Loading…
Reference in New Issue
Block a user