Files
main_backend/cpv3/infrastructure/storage/s3.py
T
2026-02-03 02:15:07 +03:00

108 lines
3.5 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import BinaryIO
import boto3 # type: ignore[import-untyped]
import boto3.session # type: ignore[import-untyped]
from botocore.config import Config # type: ignore[import-untyped]
from botocore.exceptions import ClientError # type: ignore[import-untyped]
@dataclass(frozen=True)
class S3Config:
access_key: str
secret_key: str
bucket_name: str
endpoint_url_internal: str | None
endpoint_url_public: str | None
presign_expires_seconds: int = 3600
class S3StorageBackend:
def __init__(self, cfg: S3Config) -> None:
self._cfg = cfg
self._bucket_ready = False
session = boto3.session.Session()
common = {
"aws_access_key_id": cfg.access_key,
"aws_secret_access_key": cfg.secret_key,
"region_name": "us-east-1",
"config": Config(signature_version="s3v4", s3={"addressing_style": "path"}),
}
self._client = session.client(
"s3", endpoint_url=cfg.endpoint_url_internal, **common
)
presign_endpoint = cfg.endpoint_url_public or cfg.endpoint_url_internal
self._presign_client = session.client(
"s3", endpoint_url=presign_endpoint, **common
)
def ensure_bucket(self) -> None:
if self._bucket_ready:
return
try:
self._client.head_bucket(Bucket=self._cfg.bucket_name)
except ClientError as e:
code = str(e.response.get("Error", {}).get("Code", ""))
if code in {"404", "NoSuchBucket"}:
self._client.create_bucket(Bucket=self._cfg.bucket_name)
else:
raise
self._bucket_ready = True
def upload_fileobj(
self, key: str, fileobj: BinaryIO, *, content_type: str | None
) -> None:
self.ensure_bucket()
extra_args = {"ContentType": content_type} if content_type else None
self._client.upload_fileobj(
Fileobj=fileobj,
Bucket=self._cfg.bucket_name,
Key=key,
ExtraArgs=extra_args,
)
def download_fileobj(self, key: str, fileobj: BinaryIO) -> None:
self.ensure_bucket()
self._client.download_fileobj(self._cfg.bucket_name, key, fileobj)
def exists(self, key: str) -> bool:
self.ensure_bucket()
try:
self._client.head_object(Bucket=self._cfg.bucket_name, Key=key)
return True
except ClientError as e:
code = str(e.response.get("Error", {}).get("Code", ""))
if code in {"404", "NoSuchKey"}:
return False
raise
def size(self, key: str) -> int:
self.ensure_bucket()
resp = self._client.head_object(Bucket=self._cfg.bucket_name, Key=key)
return int(resp.get("ContentLength", 0))
def delete(self, key: str) -> None:
self.ensure_bucket()
self._client.delete_object(Bucket=self._cfg.bucket_name, Key=key)
def read(self, key: str) -> bytes:
self.ensure_bucket()
resp = self._client.get_object(Bucket=self._cfg.bucket_name, Key=key)
body = resp["Body"].read()
return body
def generate_url(self, key: str) -> str:
self.ensure_bucket()
return self._presign_client.generate_presigned_url(
ClientMethod="get_object",
Params={"Bucket": self._cfg.bucket_name, "Key": key},
ExpiresIn=self._cfg.presign_expires_seconds,
)