Implements all 88 tasks for the Reaction Image Board (specs/001-reaction-image-board): - docker-compose.yml: postgres, minio, minio-init, api, ui services with healthchecks - api/: FastAPI app with SQLAlchemy 2.x async, Alembic migrations, S3/MinIO storage, full integration + unit test suite (pytest + pytest-asyncio) - ui/: Angular 19 standalone app (Library, Upload, Detail, NotFound components) - .env.example: all required environment variables - .gitignore: Python, Node, Docker, IDE, .env patterns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
47 lines
1.6 KiB
Python
47 lines
1.6 KiB
Python
from contextlib import asynccontextmanager
|
|
|
|
import aiobotocore.session
|
|
|
|
from app.config import get_settings
|
|
from app.storage.backend import StorageBackend
|
|
|
|
|
|
class S3StorageBackend(StorageBackend):
|
|
def __init__(self) -> None:
|
|
self._settings = get_settings()
|
|
self._session = aiobotocore.session.get_session()
|
|
|
|
@asynccontextmanager
|
|
async def _client(self):
|
|
s = self._settings
|
|
async with self._session.create_client(
|
|
"s3",
|
|
region_name=s.s3_region,
|
|
endpoint_url=s.s3_endpoint_url or None,
|
|
aws_access_key_id=s.s3_access_key_id,
|
|
aws_secret_access_key=s.s3_secret_access_key,
|
|
) as client:
|
|
yield client
|
|
|
|
async def put(self, key: str, data: bytes, content_type: str) -> None:
|
|
async with self._client() as client:
|
|
await client.put_object(
|
|
Bucket=self._settings.s3_bucket_name,
|
|
Key=key,
|
|
Body=data,
|
|
ContentType=content_type,
|
|
)
|
|
|
|
async def get_presigned_url(self, key: str, expires_in_seconds: int = 3600) -> str:
|
|
async with self._client() as client:
|
|
url = await client.generate_presigned_url(
|
|
"get_object",
|
|
Params={"Bucket": self._settings.s3_bucket_name, "Key": key},
|
|
ExpiresIn=expires_in_seconds,
|
|
)
|
|
return url
|
|
|
|
async def delete(self, key: str) -> None:
|
|
async with self._client() as client:
|
|
await client.delete_object(Bucket=self._settings.s3_bucket_name, Key=key)
|