import os from urllib.parse import urlparse, urlunparse from qcloud_cos import CosConfig, CosS3Client def normalize_cos_base_url(base_url: str, bucket: str, region: str) -> str: candidate = (base_url or "").rstrip("/") if not candidate and bucket and region: candidate = f"https://{bucket}.cos.{region}.myqcloud.com" if not candidate: return "" parsed = urlparse(candidate) duplicated_appid_host = f"{bucket}-appid.cos." if bucket else "" if duplicated_appid_host and parsed.netloc.startswith(duplicated_appid_host): parsed = parsed._replace( netloc=parsed.netloc.replace(duplicated_appid_host, f"{bucket}.cos.", 1), path=parsed.path.rstrip("/"), ) return urlunparse(parsed).rstrip("/") return candidate def normalize_cos_url(url: str | None, bucket: str, region: str, base_url: str | None = None) -> str | None: if not url: return url parsed = urlparse(url) if not parsed.scheme or not parsed.netloc: return url normalized_base = normalize_cos_base_url( base_url or f"{parsed.scheme}://{parsed.netloc}", bucket=bucket, region=region, ) if not normalized_base: return url normalized_parsed = urlparse(normalized_base) return urlunparse(parsed._replace(scheme=normalized_parsed.scheme, netloc=normalized_parsed.netloc)) def resolve_image_storage_key(image: dict | None) -> str | None: if not image: return None explicit_key = image.get("storage_key") if explicit_key: return explicit_key url = image.get("url") if not url: return None parsed = urlparse(url) if parsed.scheme and parsed.netloc: key = parsed.path.lstrip("/") return key or None return str(url).lstrip("/") or None class TencentCosStorageService: _instance: "TencentCosStorageService | None" = None _instance_config: tuple[str, str, str, str, str] | None = None def __init__(self, secret_id: str, secret_key: str, region: str, bucket: str, base_url: str): self.secret_id = secret_id self.secret_key = secret_key self.bucket = bucket self.region = region self.base_url = normalize_cos_base_url(base_url, bucket=bucket, region=region) config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key) self.client = CosS3Client(config) def upload_bytes(self, image_bytes: bytes, key: str, content_type: str) -> str: self.client.put_object( Bucket=self.bucket, Body=image_bytes, Key=key, ContentType=content_type, ) return f"{self.base_url}/{key}" def get_download_url(self, key: str, expires: int = 3600) -> str: return self.client.get_presigned_download_url( Bucket=self.bucket, Key=key, Expired=expires, ) @classmethod def from_env(cls) -> "TencentCosStorageService": config = ( os.getenv("TENCENT_COS_SECRET_ID", ""), os.getenv("TENCENT_COS_SECRET_KEY", ""), os.getenv("TENCENT_COS_REGION", ""), os.getenv("TENCENT_COS_BUCKET", ""), os.getenv("TENCENT_COS_BASE_URL", ""), ) if cls._instance is None or cls._instance_config != config: cls._instance = cls( secret_id=config[0], secret_key=config[1], region=config[2], bucket=config[3], base_url=config[4], ) cls._instance_config = config return cls._instance