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"] 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="cn-shanghai", config=Config(s3={"addressing_style": "virtual"}), ) 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 过期,反而打不到缓存)。仅用于公开可读的预览图。""" host = urlparse(settings.TOS["endpoint"]).netloc return f"https://{bucket or self.bucket}.{host}/{quote(object_key, safe='/')}"