"""MinIO / S3-compatible object-storage helpers. Provides thin wrappers around boto3 for bucket management, file upload and presigned-URL generation. Two clients are maintained: - ``_client`` — uses the internal Docker endpoint (``MINIO_ENDPOINT``) for all server-side operations (upload, head_bucket, create_bucket). - ``_public_client`` — used **only** for presigned URL generation. It uses ``MINIO_PUBLIC_ENDPOINT`` when set so the resulting URLs are reachable from browsers. If ``MINIO_PUBLIC_ENDPOINT`` is not configured it falls back to ``MINIO_ENDPOINT`` (backwards-compatible). """ # Import boto3 import boto3 # Import ClientError from botocore.exceptions from botocore.exceptions import ClientError # Import settings from app.config from app.config import settings # --------------------------------------------------------------------------- # Shared clients (module-level singletons) # --------------------------------------------------------------------------- _scheme = "https" if settings.MINIO_SECURE else "http" # Internal client — used for uploads and bucket management _client = boto3.client( # Literal argument value "s3", # Keyword argument: endpoint_url endpoint_url=f"{_scheme}://{settings.MINIO_ENDPOINT}", # Keyword argument: aws_access_key_id aws_access_key_id=settings.MINIO_ACCESS_KEY, # Keyword argument: aws_secret_access_key aws_secret_access_key=settings.MINIO_SECRET_KEY, # Keyword argument: region_name region_name="us-east-1", # MinIO ignores this but boto3 requires it ) # Public client — used only for presigned URL generation so URLs contain the # publicly accessible hostname instead of the internal Docker service name. _public_endpoint = settings.MINIO_PUBLIC_ENDPOINT or settings.MINIO_ENDPOINT _public_client = boto3.client( "s3", endpoint_url=f"{_scheme}://{_public_endpoint}", aws_access_key_id=settings.MINIO_ACCESS_KEY, aws_secret_access_key=settings.MINIO_SECRET_KEY, region_name="us-east-1", ) # --------------------------------------------------------------------------- # Public helpers # --------------------------------------------------------------------------- def ensure_bucket_exists() -> None: """Create the evidence bucket if it does not already exist.""" # Attempt the following; catch errors below try: # Call _client.head_bucket() _client.head_bucket(Bucket=settings.MINIO_BUCKET) # Handle ClientError except ClientError: # Call _client.create_bucket() _client.create_bucket(Bucket=settings.MINIO_BUCKET) # Define function upload_file def upload_file(content: bytes, key: str) -> str: """Upload *content* to the evidence bucket under *key*. Returns the key that was written (same as the input). """ # Call _client.put_object() _client.put_object( # Keyword argument: Bucket Bucket=settings.MINIO_BUCKET, # Keyword argument: Key Key=key, # Keyword argument: Body Body=content, ) # Return key return key def download_file(key: str) -> bytes: """Download *key* from the evidence bucket and return its raw bytes.""" response = _client.get_object(Bucket=settings.MINIO_BUCKET, Key=key) return response["Body"].read() def get_presigned_url(key: str, expiration: int = 3600) -> str: """Return a presigned GET URL for *key* valid for *expiration* seconds. Uses ``_public_client`` so the URL contains the publicly accessible hostname (``MINIO_PUBLIC_ENDPOINT``) rather than the internal Docker service name (``MINIO_ENDPOINT``). """ return _public_client.generate_presigned_url( "get_object", # Keyword argument: Params Params={"Bucket": settings.MINIO_BUCKET, "Key": key}, # Keyword argument: ExpiresIn ExpiresIn=expiration, )