init: new structure + fix lint errors
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user