Backend (replaces PHP api.php + auth.php): - FastAPI app with routers: jobs, auth, billing - Supabase JWT authentication in deps.py - Celery + Redis job queue (process_pdf_task) - MinIO S3-compatible storage service - PDF checker wrapper (delegates to enterprise_pdf_checker.py) - Stripe billing: checkout, portal, webhook handler Multi-tenancy (Phase 3): - Alembic migration 001: workspaces, workspace_members, jobs, usage_events - Row-Level Security on all tenant tables via app.workspace_id session var - Monthly quota enforcement per workspace (402 on exceeded) - Plan tiers: free(5) / pro(100) / business(unlimited) Config: - pydantic-settings based config.py (no hardcoded values) - docker-compose.yml rewritten: postgres, redis, minio, api, celery Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
57 lines
1.6 KiB
Python
57 lines
1.6 KiB
Python
"""MinIO/S3-compatible storage abstraction."""
|
|
import boto3
|
|
from botocore.config import Config
|
|
from botocore.exceptions import ClientError
|
|
from app.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
_client = None
|
|
|
|
|
|
def get_client():
|
|
global _client
|
|
if _client is None:
|
|
_client = boto3.client(
|
|
"s3",
|
|
endpoint_url=settings.storage_endpoint,
|
|
aws_access_key_id=settings.storage_access_key,
|
|
aws_secret_access_key=settings.storage_secret_key,
|
|
config=Config(signature_version="s3v4"),
|
|
)
|
|
return _client
|
|
|
|
|
|
def ensure_bucket() -> None:
|
|
client = get_client()
|
|
try:
|
|
client.head_bucket(Bucket=settings.storage_bucket)
|
|
except ClientError:
|
|
client.create_bucket(Bucket=settings.storage_bucket)
|
|
|
|
|
|
def upload_bytes(key: str, data: bytes, content_type: str = "application/octet-stream") -> str:
|
|
get_client().put_object(
|
|
Bucket=settings.storage_bucket,
|
|
Key=key,
|
|
Body=data,
|
|
ContentType=content_type,
|
|
)
|
|
return f"{settings.storage_endpoint}/{settings.storage_bucket}/{key}"
|
|
|
|
|
|
def download_bytes(key: str) -> bytes:
|
|
response = get_client().get_object(Bucket=settings.storage_bucket, Key=key)
|
|
return response["Body"].read()
|
|
|
|
|
|
def delete_object(key: str) -> None:
|
|
get_client().delete_object(Bucket=settings.storage_bucket, Key=key)
|
|
|
|
|
|
def presigned_url(key: str, expires: int = 3600) -> str:
|
|
return get_client().generate_presigned_url(
|
|
"get_object",
|
|
Params={"Bucket": settings.storage_bucket, "Key": key},
|
|
ExpiresIn=expires,
|
|
)
|