import logging from collections.abc import Callable from dataclasses import dataclass from typing import Any from campaigns.assets.dtos import AssetUploadCredentials from campaigns.assets.exceptions import AssetDoesNotExist from campaigns.assets.models import Asset from campaigns.assets.repositories import AssetRepository from campaigns.connectors.aws.s3 import S3Client from campaigns.connectors.aws.sts import STSClient from campaigns.utils.uuid import uuid_string logger = logging.getLogger(__name__) @dataclass class AssetService: s3_client: S3Client sts_client: STSClient asset_repository: AssetRepository upload_policy_func: Callable[[str], dict[str, Any]] upload_bucket_name: str upload_iam_role_arn: str upload_region_name: str async def get_asset(self, *, asset_id: str) -> Asset: asset = await self.asset_repository.get_or_none(asset_id=asset_id) if not asset: raise AssetDoesNotExist return asset async def create_asset_with_credentials( self, filename: str ) -> tuple[Asset, AssetUploadCredentials]: credentials = await self.generate_asset_upload_credentials(filename) asset = Asset() asset.id = credentials.asset_id asset.key = credentials.upload_key asset.bucket = credentials.upload_bucket asset.filename = filename self.asset_repository.add(asset) return asset, credentials async def generate_asset_upload_credentials( self, filename: str ) -> AssetUploadCredentials: asset_id = uuid_string() upload_key = Asset.make_key(asset_id=asset_id, filename=filename) policy = self.upload_policy_func(upload_key) credentials = await self.sts_client.assume_role( role_arn=self.upload_iam_role_arn, role_session_name=f"asset-{asset_id}", policy=policy, ) return AssetUploadCredentials( asset_id=asset_id, upload_bucket=self.upload_bucket_name, upload_key=upload_key, upload_region=self.upload_region_name, access_key_id=credentials.access_key_id, secret_access_key=credentials.secret_access_key, session_token=credentials.session_token, expiration=credentials.expiration, ) async def save_asset_thumbnail( self, asset: Asset, *, content: bytes, content_type: str | None = None, extension: str | None = None, ) -> Asset: content_type = content_type or asset.content_type if content_type is None: logger.error( "Thumbnail content_type is missing.", extra={"asset_id": asset.id}, ) asset.set_thumbnail_key(extension=extension) if asset.thumbnail_key: await self.s3_client.put_object( bucket=asset.bucket, key=asset.thumbnail_key, content=content, content_type=content_type, ) return asset async def delete_asset(self, asset: Asset) -> None: if asset.thumbnail_key is not None: await self.s3_client.delete_object( bucket=asset.bucket, key=asset.thumbnail_key ) await self.s3_client.delete_object(bucket=asset.bucket, key=asset.key) await self.asset_repository.delete(asset)