You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
88 lines
3.4 KiB
88 lines
3.4 KiB
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import anyio
|
|
import boto3
|
|
from botocore.config import Config
|
|
from botocore.exceptions import ClientError
|
|
|
|
|
|
class S3StorageAdapter:
|
|
def __init__(self, config: dict[str, Any]) -> None:
|
|
self.config = config
|
|
|
|
async def check(self) -> dict[str, str]:
|
|
client = self._create_client()
|
|
|
|
def _check() -> dict[str, str]:
|
|
client.head_bucket(Bucket=self.config["bucket"])
|
|
return {"status": "healthy", "detail": self.config["bucket"]}
|
|
|
|
return await anyio.to_thread.run_sync(_check)
|
|
|
|
async def put_file(self, source_path: str, storage_key: str) -> dict[str, str | None]:
|
|
client = self._create_client()
|
|
object_key = self._object_key(storage_key)
|
|
|
|
def _put() -> dict[str, str | None]:
|
|
extra_args: dict[str, Any] = {}
|
|
if self.config.get("storage_class"):
|
|
extra_args["StorageClass"] = self.config["storage_class"]
|
|
if self.config.get("server_side_encryption"):
|
|
extra_args["ServerSideEncryption"] = self.config["server_side_encryption"]
|
|
client.upload_file(source_path, self.config["bucket"], object_key, ExtraArgs=extra_args)
|
|
metadata = client.head_object(Bucket=self.config["bucket"], Key=object_key)
|
|
return {"etag": metadata.get("ETag"), "storage_key": storage_key}
|
|
|
|
return await anyio.to_thread.run_sync(_put)
|
|
|
|
async def download_file(self, storage_key: str, destination_path: Path) -> Path:
|
|
client = self._create_client()
|
|
object_key = self._object_key(storage_key)
|
|
|
|
def _download() -> Path:
|
|
destination_path.parent.mkdir(parents=True, exist_ok=True)
|
|
client.download_file(self.config["bucket"], object_key, str(destination_path))
|
|
return destination_path
|
|
|
|
return await anyio.to_thread.run_sync(_download)
|
|
|
|
async def object_exists(self, storage_key: str) -> bool:
|
|
client = self._create_client()
|
|
object_key = self._object_key(storage_key)
|
|
|
|
def _exists() -> bool:
|
|
try:
|
|
client.head_object(Bucket=self.config["bucket"], Key=object_key)
|
|
return True
|
|
except ClientError as exc:
|
|
error_code = exc.response.get("Error", {}).get("Code")
|
|
if error_code in {"404", "NoSuchKey", "NotFound"}:
|
|
return False
|
|
raise
|
|
|
|
return await anyio.to_thread.run_sync(_exists)
|
|
|
|
def _create_client(self):
|
|
client_config = None
|
|
if self.config.get("force_path_style"):
|
|
client_config = Config(s3={"addressing_style": "path"})
|
|
client_kwargs = {
|
|
"service_name": "s3",
|
|
"region_name": self.config.get("region"),
|
|
"endpoint_url": self.config.get("endpoint_url"),
|
|
"aws_access_key_id": self.config.get("access_key_id"),
|
|
"aws_secret_access_key": self.config.get("secret_access_key"),
|
|
"verify": self.config.get("verify_ssl", True),
|
|
"config": client_config,
|
|
}
|
|
cleaned_kwargs = {key: value for key, value in client_kwargs.items() if value is not None}
|
|
return boto3.client(**cleaned_kwargs)
|
|
|
|
def _object_key(self, storage_key: str) -> str:
|
|
prefix = (self.config.get("prefix") or "").strip("/")
|
|
if not prefix:
|
|
return storage_key
|
|
return f"{prefix}/{storage_key}"
|
|
|