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"))
@ROUTES.get("/api/assets/{id}/content")
@ROUTES.get("/api/assets/{id:\\d+}/content")
async def download_asset_content(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id")
try:
@ -198,7 +198,24 @@ async def upload_asset(request: web.Request) -> web.Response:
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:
asset_info_id_raw = request.match_info.get("id")
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)
@ROUTES.delete("/api/assets/{id}")
@ROUTES.delete("/api/assets/{id:\\d+}")
async def delete_asset(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id")
try:
@ -267,7 +284,7 @@ async def get_tags(request: web.Request) -> web.Response:
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:
asset_info_id_raw = request.match_info.get("id")
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)
@ROUTES.delete("/api/assets/{id}/tags")
@ROUTES.delete("/api/assets/{id:\\d+}/tags")
async def delete_asset_tags(request: web.Request) -> web.Response:
asset_info_id_raw = request.match_info.get("id")
try:

View File

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

View File

@ -24,6 +24,7 @@ from .database.services import (
asset_exists_by_hash,
get_asset_by_hash,
create_asset_info_for_existing_asset,
fetch_asset_info_asset_and_tags,
)
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
@ -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(
*, asset_info_id: int
) -> tuple[str, str, str]:

View File

@ -9,7 +9,7 @@ from typing import Any, Sequence, Optional, Iterable
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import AsyncSession
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 .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]
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]:
return await session.get(AssetCacheState, asset_hash)