add Get Asset endpoint

This commit is contained in:
bigcat88 2025-08-27 09:58:12 +03:00
parent 6fade5da38
commit 7c1b0be496
No known key found for this signature in database
GPG Key ID: 1F0BF0EC3CF22721
4 changed files with 85 additions and 8 deletions

View File

@ -49,7 +49,7 @@ async def list_assets(request: web.Request) -> web.Response:
return web.json_response(payload.model_dump(mode="json")) return web.json_response(payload.model_dump(mode="json"))
@ROUTES.get("/api/assets/{id}/content") @ROUTES.get("/api/assets/{id:\\d+}/content")
async def download_asset_content(request: web.Request) -> web.Response: async def download_asset_content(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id") asset_info_id_raw = request.match_info.get("id")
try: try:
@ -198,7 +198,24 @@ async def upload_asset(request: web.Request) -> web.Response:
return _error_response(500, "INTERNAL", "Unexpected server error.") return _error_response(500, "INTERNAL", "Unexpected server error.")
@ROUTES.put("/api/assets/{id}") @ROUTES.get("/api/assets/{id:\\d+}")
async def get_asset(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id")
try:
asset_info_id = int(asset_info_id_raw)
except Exception:
return _error_response(400, "INVALID_ID", f"AssetInfo id '{asset_info_id_raw}' is not a valid integer.")
try:
result = await assets_manager.get_asset(asset_info_id=asset_info_id)
except ValueError as ve:
return _error_response(404, "ASSET_NOT_FOUND", str(ve), {"id": asset_info_id})
except Exception:
return _error_response(500, "INTERNAL", "Unexpected server error.")
return web.json_response(result.model_dump(mode="json"), status=200)
@ROUTES.put("/api/assets/{id:\\d+}")
async def update_asset(request: web.Request) -> web.Response: async def update_asset(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id") asset_info_id_raw = request.match_info.get("id")
try: try:
@ -227,7 +244,7 @@ async def update_asset(request: web.Request) -> web.Response:
return web.json_response(result.model_dump(mode="json"), status=200) return web.json_response(result.model_dump(mode="json"), status=200)
@ROUTES.delete("/api/assets/{id}") @ROUTES.delete("/api/assets/{id:\\d+}")
async def delete_asset(request: web.Request) -> web.Response: async def delete_asset(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id") asset_info_id_raw = request.match_info.get("id")
try: try:
@ -267,7 +284,7 @@ async def get_tags(request: web.Request) -> web.Response:
return web.json_response(result.model_dump(mode="json")) return web.json_response(result.model_dump(mode="json"))
@ROUTES.post("/api/assets/{id}/tags") @ROUTES.post("/api/assets/{id:\\d+}/tags")
async def add_asset_tags(request: web.Request) -> web.Response: async def add_asset_tags(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id") asset_info_id_raw = request.match_info.get("id")
try: try:
@ -298,7 +315,7 @@ async def add_asset_tags(request: web.Request) -> web.Response:
return web.json_response(result.model_dump(mode="json"), status=200) return web.json_response(result.model_dump(mode="json"), status=200)
@ROUTES.delete("/api/assets/{id}/tags") @ROUTES.delete("/api/assets/{id:\\d+}/tags")
async def delete_asset_tags(request: web.Request) -> web.Response: async def delete_asset_tags(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id") asset_info_id_raw = request.match_info.get("id")
try: try:

View File

@ -43,7 +43,7 @@ class AssetUpdated(BaseModel):
return v.isoformat() if v else None return v.isoformat() if v else None
class AssetCreated(BaseModel): class AssetDetail(BaseModel):
id: int id: int
name: str name: str
asset_hash: str asset_hash: str
@ -54,7 +54,6 @@ class AssetCreated(BaseModel):
preview_hash: Optional[str] = None preview_hash: Optional[str] = None
created_at: Optional[datetime] = None created_at: Optional[datetime] = None
last_access_time: Optional[datetime] = None last_access_time: Optional[datetime] = None
created_new: bool
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@ -63,6 +62,10 @@ class AssetCreated(BaseModel):
return v.isoformat() if v else None return v.isoformat() if v else None
class AssetCreated(AssetDetail):
created_new: bool
class TagUsage(BaseModel): class TagUsage(BaseModel):
name: str name: str
count: int count: int

View File

@ -24,6 +24,7 @@ from .database.services import (
asset_exists_by_hash, asset_exists_by_hash,
get_asset_by_hash, get_asset_by_hash,
create_asset_info_for_existing_asset, create_asset_info_for_existing_asset,
fetch_asset_info_asset_and_tags,
) )
from .api import schemas_in, schemas_out from .api import schemas_in, schemas_out
from ._assets_helpers import get_name_and_tags_from_asset_path, ensure_within_base, resolve_destination_from_tags from ._assets_helpers import get_name_and_tags_from_asset_path, ensure_within_base, resolve_destination_from_tags
@ -140,6 +141,27 @@ async def list_assets(
) )
async def get_asset(*, asset_info_id: int) -> schemas_out.AssetDetail:
async with await create_session() as session:
res = await fetch_asset_info_asset_and_tags(session, asset_info_id=asset_info_id)
if not res:
raise ValueError(f"AssetInfo {asset_info_id} not found")
info, asset, tag_names = res
return schemas_out.AssetDetail(
id=info.id,
name=info.name,
asset_hash=info.asset_hash,
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=tag_names,
preview_hash=info.preview_hash,
user_metadata=info.user_metadata or {},
created_at=info.created_at,
last_access_time=info.last_access_time,
)
async def resolve_asset_content_for_download( async def resolve_asset_content_for_download(
*, asset_info_id: int *, asset_info_id: int
) -> tuple[str, str, str]: ) -> tuple[str, str, str]:

View File

@ -9,7 +9,7 @@ from typing import Any, Sequence, Optional, Iterable
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete, exists, func from sqlalchemy import select, delete, exists, func
from sqlalchemy.orm import contains_eager from sqlalchemy.orm import contains_eager, noload
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from .models import Asset, AssetInfo, AssetInfoTag, AssetCacheState, Tag, AssetInfoMeta, AssetLocation from .models import Asset, AssetInfo, AssetInfoTag, AssetCacheState, Tag, AssetInfoMeta, AssetLocation
@ -407,6 +407,41 @@ async def fetch_asset_info_and_asset(session: AsyncSession, *, asset_info_id: in
return pair[0], pair[1] return pair[0], pair[1]
async def fetch_asset_info_asset_and_tags(
session: AsyncSession,
*,
asset_info_id: int,
) -> Optional[tuple[AssetInfo, Asset, list[str]]]:
"""Fetch AssetInfo, its Asset, and all tag names.
Returns:
(AssetInfo, Asset, [tag_names]) or None if the asset_info_id does not exist.
"""
stmt = (
select(AssetInfo, Asset, Tag.name)
.join(Asset, Asset.hash == AssetInfo.asset_hash)
.join(AssetInfoTag, AssetInfoTag.asset_info_id == AssetInfo.id, isouter=True)
.join(Tag, Tag.name == AssetInfoTag.tag_name, isouter=True)
.where(AssetInfo.id == asset_info_id)
.options(noload(AssetInfo.tags))
.order_by(Tag.name.asc())
)
rows = (await session.execute(stmt)).all()
if not rows:
return None
# First row contains the mapped entities; tags may repeat across rows
first_info, first_asset, _ = rows[0]
tags: list[str] = []
seen: set[str] = set()
for _info, _asset, tag_name in rows:
if tag_name and tag_name not in seen:
seen.add(tag_name)
tags.append(tag_name)
return first_info, first_asset, tags
async def get_cache_state_by_asset_hash(session: AsyncSession, *, asset_hash: str) -> Optional[AssetCacheState]: async def get_cache_state_by_asset_hash(session: AsyncSession, *, asset_hash: str) -> Optional[AssetCacheState]:
return await session.get(AssetCacheState, asset_hash) return await session.get(AssetCacheState, asset_hash)