import uuid import httpx from config.server_configs import storage_config # 허용 content-type -> 저장 확장자. 목록 밖이면 업로드 거부. _EXT_BY_TYPE = { "image/jpeg": "jpg", "image/png": "png", "image/gif": "gif", "image/webp": "webp", } # 업로드용 공유 클라이언트(연결 풀링). _shared_client: httpx.AsyncClient | None = None def _get_client() -> httpx.AsyncClient: global _shared_client if _shared_client is None or _shared_client.is_closed: _shared_client = httpx.AsyncClient( timeout=httpx.Timeout(60.0, connect=10.0), limits=httpx.Limits(max_keepalive_connections=10, max_connections=20), ) return _shared_client def is_allowed_image(content_type: str) -> bool: return content_type in _EXT_BY_TYPE async def upload_image(company_id: str, content: bytes, content_type: str) -> str: """Azure Blob 에 이미지를 올리고 접근 URL 을 돌려준다. blob 경로: //items/. — company_id 로 멀티테넌트 격리. 실패 시 예외를 던진다(호출측에서 IMAGE_UPLOAD_FAILED 처리). 저장값은 SAS 를 뗀 public URL.""" ext = _EXT_BY_TYPE.get(content_type, "bin") blob_path = f"{storage_config.blob_root}/{company_id}/items/{uuid.uuid4().hex}.{ext}" base = storage_config.azure_blob_base_url.rstrip("/") sas = storage_config.azure_blob_sas_token.strip("?'\"") public_url = f"{base}/{blob_path}" upload_url = f"{public_url}?{sas}" headers = {"Content-Type": content_type, "x-ms-blob-type": "BlockBlob"} resp = await _get_client().put(upload_url, content=content, headers=headers) if resp.status_code not in (200, 201): raise RuntimeError(f"Azure Blob upload failed: status={resp.status_code} body={resp.text[:200]}") return public_url