ssulbox/backend/app/blob_client.py

63 lines
2.5 KiB
Python

# -*- coding: utf-8 -*-
"""Azure Blob Storage 업로더 (ADO2 방식 슬림 포팅).
SDK 없이 SAS 토큰을 붙여 REST PUT 한다(aiohttp). 컨테이너가 공개 읽기이므로
재생 URL엔 SAS가 없고, 업로드에만 write SAS가 필요하다. SAS 미설정 시 비활성.
blob 경로: {BASE_URL}/{PREFIX}/{owner}/{job_id}/video/{파일명}
DB(Content.video)엔 SAS 없는 공개 URL을 저장한다.
"""
import re
from pathlib import Path
import aiohttp
from .config import settings
_UPLOAD_TIMEOUT = aiohttp.ClientTimeout(total=180, connect=10)
_SANITIZE_RE = re.compile(r"[^A-Za-z0-9._-]+")
def enabled() -> bool:
return settings.azure_blob_enabled
def sanitize_filename(name: str) -> str:
"""URL·blob 안전 파일명(한글/공백/특수문자 → '_'). 확장자 유지."""
stem = Path(name).stem
ext = Path(name).suffix.lower()
stem = _SANITIZE_RE.sub("_", stem).strip("_") or "video"
return f"{stem}{ext}"
def _blob_path(owner_uuid: str | None, job_id: str, filename: str) -> str:
owner = owner_uuid or "public"
fn = sanitize_filename(filename)
return f"{settings.AZURE_BLOB_PREFIX}/{owner}/{job_id}/video/{fn}"
def public_url(owner_uuid: str | None, job_id: str, filename: str) -> str:
return f"{settings.AZURE_BLOB_BASE_URL.rstrip('/')}/{_blob_path(owner_uuid, job_id, filename)}"
async def upload_video(local_path: Path, owner_uuid: str | None, job_id: str) -> str | None:
"""mp4를 블롭에 업로드하고 공개 URL 반환. 실패/비활성 시 None."""
if not enabled() or not local_path.is_file():
return None
blob = _blob_path(owner_uuid, job_id, local_path.name)
sas = (settings.AZURE_BLOB_SAS_TOKEN or "").strip("?'\"")
upload_url = f"{settings.AZURE_BLOB_BASE_URL.rstrip('/')}/{blob}?{sas}"
headers = {"Content-Type": "video/mp4", "x-ms-blob-type": "BlockBlob"}
try:
data = local_path.read_bytes()
async with aiohttp.ClientSession(timeout=_UPLOAD_TIMEOUT) as s:
async with s.put(upload_url, data=data, headers=headers) as r:
if r.status in (200, 201):
return f"{settings.AZURE_BLOB_BASE_URL.rstrip('/')}/{blob}"
body = (await r.text())[:300]
print(f"[blob] upload failed {r.status}: {body}")
return None
except Exception as e: # 네트워크/타임아웃 — fallback
print(f"[blob] upload error: {type(e).__name__}: {e}")
return None