from dataclasses import dataclass from typing import BinaryIO from urllib.parse import quote, urlparse import boto3 from botocore.config import Config from django.conf import settings @dataclass(frozen=True) class StoredObject: bucket: str object_key: str content_type: str size_bytes: int class TosStorage: def __init__(self) -> None: tos = settings.TOS self.bucket = tos["bucket"] config_kwargs = { "signature_version": "s3v4", "s3": {"addressing_style": "virtual"}, } try: client_config = Config( **config_kwargs, request_checksum_calculation="when_required", response_checksum_validation="when_required", ) except TypeError: client_config = Config(**config_kwargs) self.client = boto3.client( "s3", endpoint_url=tos["endpoint"], aws_access_key_id=tos["access_key_id"], aws_secret_access_key=tos["secret_access_key"], region_name=tos.get("region") or "cn-shanghai", config=client_config, ) def upload_fileobj(self, *, fileobj: BinaryIO, object_key: str, content_type: str) -> StoredObject: fileobj.seek(0, 2) size = fileobj.tell() fileobj.seek(0) self.client.upload_fileobj( fileobj, self.bucket, object_key, # object_key 按 asset id 唯一、内容不可变 → 长效强缓存 + immutable: # 浏览器/CDN 永不重拉,前端重渲染(背景图重新赋值)也命中缓存,不再"每次生成全页图闪一下重载"。 ExtraArgs={"ContentType": content_type, "CacheControl": "public, max-age=31536000, immutable"}, ) return StoredObject( bucket=self.bucket, object_key=object_key, content_type=content_type, size_bytes=size, ) def presigned_get_url(self, *, object_key: str, expires_in: int = 3600) -> str: return self.client.generate_presigned_url( "get_object", Params={"Bucket": self.bucket, "Key": object_key}, ExpiresIn=expires_in, ) def public_url(self, *, object_key: str, bucket: str | None = None) -> str: """桶公读时的虚拟主机式直链:https://{bucket}.{host}/{key}。 相比预签名 URL:不签名(省 boto3 客户端/签名开销)、且 URL 稳定可被浏览器/CDN 缓存 (签名链每次都变、1h 过期,反而打不到缓存)。仅用于公开可读的预览图。 历史文件的 bucket 仍是火山引擎旧桶,必须用旧域名,不能拼到当前阿里云地址上。""" bucket_name = bucket or self.bucket endpoint = settings.TOS.get("endpoint") or "" legacy_bucket = settings.TOS.get("legacy_bucket") or "" legacy_endpoint = settings.TOS.get("legacy_endpoint") or "" if legacy_bucket and legacy_endpoint and bucket_name == legacy_bucket: endpoint = legacy_endpoint host = urlparse(endpoint).netloc return f"https://{bucket_name}.{host}/{quote(object_key, safe='/')}"