Add media (object storage) and the background worker (Phase 1)
Media model + migration; an ObjectStore interface with an S3/MinIO (boto3) implementation behind the service layer. Upload (multipart) stores bytes in object storage + a metadata row (checksum, size, content-type, optional attach to person/event/source); list returns presigned URLs; delete is soft. Editor-gated, privacy-filtered, audited. 24 tests pass (object store faked). Introduces the worker container (same image, 'python -m app.worker'): its first job is the scheduled 30-day soft-delete purge across tables + media object cleanup. Compose gains worker + S3 env on backend/worker; dev override builds the worker too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -14,9 +14,10 @@ from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
import app.models # noqa: F401 — register all models on Base.metadata
|
||||
from app.api.deps import get_mailer
|
||||
from app.api.deps import get_mailer, get_objectstore
|
||||
from app.core.db import get_session
|
||||
from app.integrations.mailer.base import Mailer
|
||||
from app.integrations.objectstore.base import ObjectStore
|
||||
from app.main import app
|
||||
from app.models import Base
|
||||
|
||||
@@ -35,7 +36,25 @@ class CapturingMailer(Mailer):
|
||||
self.resets.append((to, link))
|
||||
|
||||
|
||||
class FakeObjectStore(ObjectStore):
|
||||
def __init__(self) -> None:
|
||||
self.objects: dict[str, tuple[bytes, str]] = {}
|
||||
|
||||
async def ensure_bucket(self) -> None:
|
||||
pass
|
||||
|
||||
async def put_object(self, *, key: str, data: bytes, content_type: str) -> None:
|
||||
self.objects[key] = (data, content_type)
|
||||
|
||||
async def presigned_get_url(self, *, key: str) -> str:
|
||||
return f"https://objects.test/{key}"
|
||||
|
||||
async def delete_object(self, *, key: str) -> None:
|
||||
self.objects.pop(key, None)
|
||||
|
||||
|
||||
_mailer = CapturingMailer()
|
||||
_store = FakeObjectStore()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -61,8 +80,10 @@ async def client():
|
||||
|
||||
_mailer.verifications.clear()
|
||||
_mailer.resets.clear()
|
||||
_store.objects.clear()
|
||||
app.dependency_overrides[get_session] = _override_session
|
||||
app.dependency_overrides[get_mailer] = lambda: _mailer
|
||||
app.dependency_overrides[get_objectstore] = lambda: _store
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as http_client:
|
||||
|
||||
Reference in New Issue
Block a user