diff --git a/app/assets/api/routes.py b/app/assets/api/routes.py index 76ebf0d98..f176b4f01 100644 --- a/app/assets/api/routes.py +++ b/app/assets/api/routes.py @@ -380,6 +380,7 @@ 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"])) @@ -406,3 +407,28 @@ async def set_asset_preview(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.delete(f"/api/assets/{{id:{UUID_RE}}}") +async def delete_asset(request: web.Request) -> web.Response: + asset_info_id = str(uuid.UUID(request.match_info["id"])) + delete_content = request.query.get("delete_content") + delete_content = True if delete_content is None else delete_content.lower() not in {"0", "false", "no"} + + try: + deleted = manager.delete_asset_reference( + asset_info_id=asset_info_id, + owner_id=USER_MANAGER.get_request_user_id(request), + delete_content_if_orphan=delete_content, + ) + except Exception: + logging.exception( + "delete_asset_reference 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.") + + if not deleted: + return _error_response(404, "ASSET_NOT_FOUND", f"AssetInfo {asset_info_id} not found.") + return web.Response(status=204) diff --git a/app/assets/database/queries.py b/app/assets/database/queries.py index 8a6905783..1d50c6054 100644 --- a/app/assets/database/queries.py +++ b/app/assets/database/queries.py @@ -119,7 +119,11 @@ def apply_metadata_filter( return stmt -def asset_exists_by_hash(session: Session, asset_hash: str) -> bool: +def asset_exists_by_hash( + session: Session, + *, + asset_hash: str, +) -> bool: """ Check if an asset with a given hash exists in database. """ @@ -131,13 +135,35 @@ def asset_exists_by_hash(session: Session, asset_hash: str) -> bool: return row is not None -def get_asset_by_hash(session: Session, *, asset_hash: str) -> Asset | None: +def asset_info_exists_for_asset_id( + session: Session, + *, + asset_id: str, +) -> bool: + q = ( + select(sa.literal(True)) + .select_from(AssetInfo) + .where(AssetInfo.asset_id == asset_id) + .limit(1) + ) + return (session.execute(q)).first() is not None + + +def get_asset_by_hash( + session: Session, + *, + asset_hash: str, +) -> Asset | None: return ( session.execute(select(Asset).where(Asset.hash == asset_hash).limit(1)) ).scalars().first() -def get_asset_info_by_id(session: Session, asset_info_id: str) -> AssetInfo | None: +def get_asset_info_by_id( + session: Session, + *, + asset_info_id: str, +) -> AssetInfo | None: return session.get(AssetInfo, asset_info_id) @@ -717,6 +743,19 @@ def update_asset_info_full( return info +def delete_asset_info_by_id( + session: Session, + *, + asset_info_id: str, + owner_id: str, +) -> bool: + stmt = sa.delete(AssetInfo).where( + AssetInfo.id == asset_info_id, + visible_owner_clause(owner_id), + ) + return int((session.execute(stmt)).rowcount or 0) > 0 + + def list_tags_with_usage( session: Session, prefix: str | None = None, @@ -811,6 +850,7 @@ def remove_missing_tag_for_asset_id( ) ) + def set_asset_info_preview( session: Session, *, diff --git a/app/assets/manager.py b/app/assets/manager.py index 62e787477..bcda0df08 100644 --- a/app/assets/manager.py +++ b/app/assets/manager.py @@ -7,6 +7,7 @@ from app.database.db import create_session from app.assets.api import schemas_out, schemas_in from app.assets.database.queries import ( asset_exists_by_hash, + asset_info_exists_for_asset_id, get_asset_by_hash, get_asset_info_by_id, fetch_asset_info_asset_and_tags, @@ -14,6 +15,7 @@ from app.assets.database.queries import ( create_asset_info_for_existing_asset, touch_asset_info_by_id, update_asset_info_full, + delete_asset_info_by_id, list_cache_states_by_asset_id, list_asset_infos_page, list_tags_with_usage, @@ -23,7 +25,7 @@ from app.assets.database.queries import ( set_asset_info_preview, ) from app.assets.helpers import resolve_destination_from_tags, ensure_within_base - +from app.assets.database.models import Asset import app.assets.hashing as hashing @@ -358,6 +360,39 @@ def set_asset_preview( ) +def delete_asset_reference(*, asset_info_id: str, owner_id: str, delete_content_if_orphan: bool = True) -> bool: + with create_session() as session: + info_row = get_asset_info_by_id(session, asset_info_id=asset_info_id) + asset_id = info_row.asset_id if info_row else None + deleted = delete_asset_info_by_id(session, asset_info_id=asset_info_id, owner_id=owner_id) + if not deleted: + session.commit() + return False + + if not delete_content_if_orphan or not asset_id: + session.commit() + return True + + still_exists = asset_info_exists_for_asset_id(session, asset_id=asset_id) + if still_exists: + session.commit() + return True + + states = list_cache_states_by_asset_id(session, asset_id=asset_id) + file_paths = [s.file_path for s in (states or []) if getattr(s, "file_path", None)] + + asset_row = session.get(Asset, asset_id) + if asset_row is not None: + session.delete(asset_row) + + session.commit() + for p in file_paths: + with contextlib.suppress(Exception): + if p and os.path.isfile(p): + os.remove(p) + return True + + def create_asset_from_hash( *, hash_str: str,