import logging from pathlib import Path from typing import Optional from urllib.parse import quote from .config import Settings logger = logging.getLogger(__name__) class VolcanoOSSUploader: """Upload course videos to Volcano Engine Object Storage (TOS).""" def __init__(self, settings: Settings): self.settings = settings @property def enabled(self) -> bool: return self.settings.volcano_oss_enabled def object_key(self, video_hash: str, stored_filename: str) -> str: normalized_hash = video_hash.strip().lower() suffix = Path(stored_filename).suffix or ".mp4" return f"{self.settings.volcano_oss_key_prefix}{normalized_hash}{suffix}" def public_url(self, object_key: str) -> str: return self.settings.volcano_oss_public_url(object_key) def upload( self, *, video_hash: str, stored_filename: str, local_path: Path, content_type: Optional[str] = None, ) -> str: """Upload one local file and return its permanent public URL.""" if not self.enabled: raise RuntimeError("Volcano OSS is not configured.") if not local_path.is_file(): raise FileNotFoundError(f"Cannot upload a missing video: {local_path}") # Keep the SDK import optional for development machines that do not run # the production service. It is declared in sentence_api/requirements.txt. try: import tos except ImportError as exc: raise RuntimeError( "The Volcano TOS SDK is not installed. Run: pip install tos" ) from exc object_key = self.object_key(video_hash, stored_filename) endpoint = self.settings.volcano_oss_endpoint client = tos.TosClientV2( self.settings.volcano_oss_access_key, self.settings.volcano_oss_secret_key, endpoint, self.settings.volcano_oss_region, ) try: client.put_object_from_file( self.settings.volcano_oss_bucket, object_key, str(local_path), ) except Exception: logger.exception( "Volcano OSS upload failed: bucket=%s key=%s endpoint=%s", self.settings.volcano_oss_bucket, object_key, endpoint, ) raise logger.info("Uploaded %s to Volcano OSS object %s", local_path.name, object_key) return self.public_url(object_key)