from collections.abc import Sequence import httpx from anydi import singleton from fansifter_common.utils.uuid import uuid_string from ows_text_campaigns.adapters.aws.s3 import S3Client from ows_text_campaigns.adapters.aws.sts import STSClient from ows_text_campaigns.assets.exceptions import AssetNotFoundError from ows_text_campaigns.assets.models import Asset from ows_text_campaigns.assets.repositories import AssetRepository from ows_text_campaigns.assets.types import ( BinaryAsset, ImageAsset, UploadAsset, UploadCredentials, ) from ows_text_campaigns.assets.utils import update_asset_key from ows_text_campaigns.config import Settings @singleton class AssetService: def __init__( self, s3_client: S3Client, sts_client: STSClient, repository: AssetRepository, settings: Settings, ) -> None: self.s3_client = s3_client self.sts_client = sts_client self.repository = repository self.settings = settings def get_asset_from_s3(self, key: str) -> BinaryAsset: response = self.s3_client.get_object( bucket=self.settings.assets_bucket_name, key=key ) filename = response["Metadata"].get("original-filename") if filename is None: filename = key.split("/")[-1] return BinaryAsset.from_data(response["Body"].read(), filename=filename) def upload_asset_to_s3( self, asset: UploadAsset, *, key: str, original_filename: str | None = None ) -> None: self.s3_client.put_object( bucket=self.settings.assets_bucket_name, key=key, body=asset.data, content_type=asset.content_type, metadata={"original-filename": original_filename} if original_filename else None, ) def delete_asset_from_s3(self, key: str) -> None: self.s3_client.delete_object(bucket=self.settings.assets_bucket_name, key=key) @staticmethod def get_asset_from_url(url: str) -> BinaryAsset: response = httpx.get(url) response.raise_for_status() return BinaryAsset.from_data(response.content) def generate_upload_credentials(self, key: str) -> UploadCredentials: # Assume the role for uploading credentials = self.sts_client.assume_role( role_arn=self.settings.assets_iam_role_arn, role_session_name=f"upload-{uuid_string()}", policy=self.settings.get_asset_upload_policy(key), ) return UploadCredentials( key=key, bucket=self.settings.assets_bucket_name, region=self.settings.aws_region_name, access_key_id=credentials.access_key_id, secret_access_key=credentials.secret_access_key, session_token=credentials.session_token, expires_at=credentials.expiration, ) def create_and_upload_image_to_s3( self, image: ImageAsset, key: str, *, object_type: str, object_id: str, identity_id: str | None = None, ) -> Asset: width, height = image.size original_filename = image.filename or key.split("/")[-1] asset = Asset( key=key, object_type=object_type, object_id=object_id, original_filename=original_filename, content_type=image.content_type, extension=image.extension, file_size=image.file_size, image_width=width, image_height=height, created_by=identity_id, ) # Change asset key if image.is_processed: asset.key = update_asset_key( key, size=image.size, extension=image.extension ) self.upload_asset_to_s3( image, key=asset.key, original_filename=original_filename ) self.repository.add(asset) return asset def delete_asset(self, asset: Asset) -> None: self.delete_asset_from_s3(asset.key) self.repository.delete(asset) def save_asset(self, asset: Asset) -> Asset: """Save an asset to the repository.""" self.repository.save(asset) return asset def get_asset_for( self, *, asset_id: str, object_type: str, object_id: str ) -> Asset: asset = self.repository.get_by_id_for_object( asset_id=asset_id, object_type=object_type, object_id=object_id, ) if asset is None: raise AssetNotFoundError return asset def get_assets_for( self, *, asset_ids: list[str], object_type: str, object_id: str ) -> Sequence[Asset]: return self.repository.find_by_ids_for_object( asset_ids=asset_ids, object_type=object_type, object_id=object_id, )