from dataclasses import dataclass import boto3 from botocore.client import Config from app.config import Settings @dataclass(frozen=True) class PresignedUpload: method: str url: str object_key: str expires_in: int class S3Storage: def __init__(self, settings: Settings) -> None: self.settings = settings @property def configured(self) -> bool: values = ( self.settings.s3_endpoint, self.settings.s3_access_key_id, self.settings.s3_secret_access_key, ) return all(values) and not any(value.startswith("replace_with_") for value in values) def _client(self, public: bool = False): endpoint = ( self.settings.s3_public_endpoint if public and self.settings.s3_public_endpoint else self.settings.s3_endpoint ) return boto3.client( "s3", endpoint_url=endpoint, aws_access_key_id=self.settings.s3_access_key_id, aws_secret_access_key=self.settings.s3_secret_access_key, region_name=self.settings.s3_region, config=Config( signature_version="s3v4", s3={"addressing_style": "path" if self.settings.s3_force_path_style else "auto"}, ), ) def presign_input_upload(self, object_key: str, content_type: str) -> PresignedUpload: if not self.configured: raise RuntimeError("MinIO/S3 is not configured.") url = self._client(public=True).generate_presigned_url( "put_object", Params={ "Bucket": self.settings.s3_bucket_inputs, "Key": object_key, "ContentType": content_type, }, ExpiresIn=self.settings.s3_presigned_url_ttl_seconds, ) return PresignedUpload( method="PUT", url=url, object_key=object_key, expires_in=self.settings.s3_presigned_url_ttl_seconds, )