Files
wrapped/app/services/storage.py
T
Sergey Antropoff e4240a7be5 Init
2026-07-17 15:57:36 +03:00

90 lines
3.2 KiB
Python

from __future__ import annotations
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator
from aiobotocore.session import get_session
from app.config import get_settings
class ObjectStorage:
def __init__(self) -> None:
self.settings = get_settings()
def _client_kwargs(self) -> dict[str, Any]:
return {
"endpoint_url": self.settings.s3_endpoint_url,
"aws_access_key_id": self.settings.s3_access_key,
"aws_secret_access_key": self.settings.s3_secret_key,
"region_name": self.settings.s3_region,
"use_ssl": self.settings.s3_use_ssl,
}
@asynccontextmanager
async def client(self) -> AsyncIterator[Any]:
session = get_session()
async with session.create_client("s3", **self._client_kwargs()) as client:
yield client
async def ensure_bucket(self) -> None:
if not self.settings.s3_create_bucket:
return
async with self.client() as client:
try:
await client.head_bucket(Bucket=self.settings.s3_bucket)
except Exception:
await client.create_bucket(Bucket=self.settings.s3_bucket)
async def put_bytes(self, key: str, data: bytes, content_type: str = "application/octet-stream") -> None:
async with self.client() as client:
await client.put_object(
Bucket=self.settings.s3_bucket,
Key=key,
Body=data,
ContentType=content_type,
)
async def get_bytes(self, key: str) -> bytes:
async with self.client() as client:
resp = await client.get_object(Bucket=self.settings.s3_bucket, Key=key)
async with resp["Body"] as stream:
return await stream.read()
async def delete(self, key: str) -> None:
async with self.client() as client:
await client.delete_object(Bucket=self.settings.s3_bucket, Key=key)
async def delete_prefix(self, prefix: str = "wraps/") -> int:
"""Delete all objects under prefix. Returns number of deleted keys."""
deleted = 0
async with self.client() as client:
token: str | None = None
while True:
kwargs: dict[str, Any] = {
"Bucket": self.settings.s3_bucket,
"Prefix": prefix,
"MaxKeys": 1000,
}
if token:
kwargs["ContinuationToken"] = token
resp = await client.list_objects_v2(**kwargs)
contents = resp.get("Contents") or []
if contents:
# delete_objects accepts up to 1000 keys
await client.delete_objects(
Bucket=self.settings.s3_bucket,
Delete={
"Objects": [{"Key": obj["Key"]} for obj in contents],
"Quiet": True,
},
)
deleted += len(contents)
if not resp.get("IsTruncated"):
break
token = resp.get("NextContinuationToken")
return deleted
storage = ObjectStorage()