import uuid, os from dataclasses import dataclass import boto3 from botocore.errorfactory import ClientError from .base_storage import Storage from dataclasses import dataclass from loguru import logger @dataclass class S3Config: bucket: str region: str key: str secret: str folder: str = "" endpoint_url: str = "https://{region}.digitaloceanspaces.com" cdn_url: str = "https://{bucket}.{region}.cdn.digitaloceanspaces.com/{key}" private: bool = False key_path: str = "default" # 'default' uses full naming, 'random' uses generated uuid class S3Storage(Storage): def __init__(self, config: S3Config): self.bucket = config.bucket self.region = config.region self.folder = config.folder self.private = config.private self.cdn_url = config.cdn_url self.key_path = config.key_path self.key_dict = {} self.s3 = boto3.client( 's3', region_name=config.region, endpoint_url=config.endpoint_url.format(region=config.region), aws_access_key_id=config.key, aws_secret_access_key=config.secret ) def _get_path(self, key): """ Depends on the self.key_path configuration: * random - assigns a random UUID which can be used in conjunction with "private=false" to have unguessable documents publicly available -> self.folder/randomUUID * default -> defaults to self.folder/key """ # defaults to /key final_key = key if self.key_path == "random": if key not in self.key_dict: ext = os.path.splitext(key)[1] self.key_dict[key] = f"{str(uuid.uuid4())}{ext}" final_key = self.key_dict[key] return os.path.join(self.folder, final_key) def get_cdn_url(self, key): return self.cdn_url.format(bucket=self.bucket, region=self.region, key=self._get_path(key)) def exists(self, key): try: self.s3.head_object(Bucket=self.bucket, Key=self._get_path(key)) return True except ClientError: return False def uploadf(self, file, key, **kwargs): if self.private: extra_args = kwargs.get("extra_args", {}) else: extra_args = kwargs.get("extra_args", {'ACL': 'public-read'}) self.s3.upload_fileobj(file, Bucket=self.bucket, Key=self._get_path(key), ExtraArgs=extra_args)