# services/comfy-agent/app/storage.py import mimetypes import os from pathlib import Path from .config import settings try: import boto3 from botocore.client import Config except Exception: # pragma: no cover - optional runtime dependency fallback boto3 = None Config = None _s3_client = None _s3_bucket: str | None = None _s3_prefix: str | None = None def _s3_value(primary: str, fallback: str) -> str: return (primary or fallback).strip() def _build_endpoint_url(endpoint: str) -> str: if endpoint.startswith("http://") or endpoint.startswith("https://"): return endpoint scheme = "https" if settings.S3_SECURE else "http" return f"{scheme}://{endpoint}" def _ensure_bucket(client, bucket: str) -> None: try: client.head_bucket(Bucket=bucket) return except Exception: pass client.create_bucket(Bucket=bucket) def init_object_storage() -> None: global _s3_client, _s3_bucket, _s3_prefix endpoint = _s3_value(settings.S3_ENDPOINT, settings.MINIO_ENDPOINT) bucket = _s3_value(settings.S3_BUCKET, settings.MINIO_BUCKET) access_key = _s3_value(settings.S3_ACCESS_KEY, settings.MINIO_ACCESS_KEY) secret_key = _s3_value(settings.S3_SECRET_KEY, settings.MINIO_SECRET_KEY) if not endpoint or not bucket or not access_key or not secret_key: _s3_client = None _s3_bucket = None _s3_prefix = None return if boto3 is None or Config is None: print("S3 storage disabled: boto3 is not installed") return endpoint_url = _build_endpoint_url(endpoint) s3_config = Config( signature_version="s3v4", s3={"addressing_style": "path" if settings.S3_FORCE_PATH_STYLE else "auto"}, ) client = boto3.client( "s3", endpoint_url=endpoint_url, region_name=settings.S3_REGION, aws_access_key_id=access_key, aws_secret_access_key=secret_key, use_ssl=settings.S3_SECURE, config=s3_config, ) _ensure_bucket(client, bucket) _s3_client = client _s3_bucket = bucket _s3_prefix = settings.S3_PREFIX.strip("/") print(f"S3 storage enabled: endpoint={endpoint_url} bucket={bucket}") def ensure_storage() -> None: Path(settings.STORAGE_PATH).mkdir(parents=True, exist_ok=True) def make_job_dir(job_id: str) -> str: ensure_storage() d = os.path.join(settings.STORAGE_PATH, job_id) Path(d).mkdir(parents=True, exist_ok=True) return d def public_url(job_id: str, filename: str) -> str: return f"{settings.PUBLIC_BASE_URL}/{job_id}/{filename}" def publish_result_url(job_id: str, filename: str, local_path: str) -> str: if _s3_client is None or _s3_bucket is None: return public_url(job_id, filename) object_key = f"{job_id}/{filename}" if _s3_prefix: object_key = f"{_s3_prefix}/{object_key}" content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" try: _s3_client.upload_file( local_path, _s3_bucket, object_key, ExtraArgs={"ContentType": content_type}, ) return _s3_client.generate_presigned_url( ClientMethod="get_object", Params={"Bucket": _s3_bucket, "Key": object_key}, ExpiresIn=settings.S3_URL_TTL_S, ) except Exception as e: print(f"S3 upload/presign failed for job={job_id}: {e}") return public_url(job_id, filename)