Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Evidence download:
- Replace presigned MinIO URLs with backend proxy endpoint
GET /api/v1/evidence/{id}/file streams the file through the backend
so MinIO never needs to be publicly accessible from browsers
- Add download_file() helper to storage.py (internal boto3 get_object)
- download_url in EvidenceOut now points to the proxy endpoint
Jira attachment:
- Fix add_attachment call: use add_attachment_object(issue_key, BytesIO)
instead of add_attachment(issue_key, filename=..., content=...) which
had wrong keyword args for the installed atlassian-python-api version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
91 lines
3.1 KiB
Python
91 lines
3.1 KiB
Python
"""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
|
|
from botocore.exceptions import ClientError
|
|
|
|
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(
|
|
"s3",
|
|
endpoint_url=f"{_scheme}://{settings.MINIO_ENDPOINT}",
|
|
aws_access_key_id=settings.MINIO_ACCESS_KEY,
|
|
aws_secret_access_key=settings.MINIO_SECRET_KEY,
|
|
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."""
|
|
try:
|
|
_client.head_bucket(Bucket=settings.MINIO_BUCKET)
|
|
except ClientError:
|
|
_client.create_bucket(Bucket=settings.MINIO_BUCKET)
|
|
|
|
|
|
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).
|
|
"""
|
|
_client.put_object(
|
|
Bucket=settings.MINIO_BUCKET,
|
|
Key=key,
|
|
Body=content,
|
|
)
|
|
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",
|
|
Params={"Bucket": settings.MINIO_BUCKET, "Key": key},
|
|
ExpiresIn=expiration,
|
|
)
|