[Spec Kit] Implementation progress
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>
This commit is contained in:
0
api/app/storage/__init__.py
Normal file
0
api/app/storage/__init__.py
Normal file
15
api/app/storage/backend.py
Normal file
15
api/app/storage/backend.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class StorageBackend(ABC):
|
||||
@abstractmethod
|
||||
async def put(self, key: str, data: bytes, content_type: str) -> None:
|
||||
"""Store object at key with given content type."""
|
||||
|
||||
@abstractmethod
|
||||
async def get_presigned_url(self, key: str, expires_in_seconds: int = 3600) -> str:
|
||||
"""Return a pre-signed URL valid for expires_in_seconds."""
|
||||
|
||||
@abstractmethod
|
||||
async def delete(self, key: str) -> None:
|
||||
"""Delete object at key."""
|
||||
46
api/app/storage/s3_backend.py
Normal file
46
api/app/storage/s3_backend.py
Normal file
@@ -0,0 +1,46 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user