|
@@ -291,6 +291,11 @@ S3_BUCKET = os.environ.get("S3_BUCKET", "storage-s3-nkkj")
|
|
|
S3_REGION = os.environ.get("S3_REGION", "cn-north-4")
|
|
S3_REGION = os.environ.get("S3_REGION", "cn-north-4")
|
|
|
S3_HOST = os.environ.get("S3_ENDPOINT", "https://obs.cn-north-4.myhuaweicloud.com") \
|
|
S3_HOST = os.environ.get("S3_ENDPOINT", "https://obs.cn-north-4.myhuaweicloud.com") \
|
|
|
.replace("https://", "").rstrip("/")
|
|
.replace("https://", "").rstrip("/")
|
|
|
|
|
+# 公网访问铁律(2026-08-29 用户定调): 对外链接必须 s3.fmode.cn(全小写)。
|
|
|
|
|
+# 该域名走华为云CDN回源OBS静态网站托管(Content-Disposition:inline), OBS桶本身不渲染网页,
|
|
|
|
|
+# 直接用OBS原生域名会破坏网页渲染与统一域名。仅签名上传仍走 S3_UPLOAD_HOST。
|
|
|
|
|
+S3_PUBLIC_HOST = os.environ.get("S3_PUBLIC_HOST", "s3.fmode.cn")
|
|
|
|
|
+S3_UPLOAD_HOST = f"{S3_BUCKET}.{S3_HOST}"
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sigv4(method: str, key: str, payload: bytes, cred: dict,
|
|
def _sigv4(method: str, key: str, payload: bytes, cred: dict,
|
|
@@ -331,12 +336,12 @@ def _sigv4(method: str, key: str, payload: bytes, cred: dict,
|
|
|
|
|
|
|
|
def s3_put_bytes(key: str, payload: bytes, content_type: str = "application/octet-stream",
|
|
def s3_put_bytes(key: str, payload: bytes, content_type: str = "application/octet-stream",
|
|
|
public_read: bool = True) -> str:
|
|
public_read: bool = True) -> str:
|
|
|
- """上传字节到 S3_BUCKET, 返回可公开访问的 URL。"""
|
|
|
|
|
|
|
+ """上传字节到 S3_BUCKET, 返回可公开访问的 URL(s3.fmode.cn, 见 S3_PUBLIC_HOST 铁律)。"""
|
|
|
cred = get_storage_credentials(f"user/{get_userid()}/")
|
|
cred = get_storage_credentials(f"user/{get_userid()}/")
|
|
|
hdrs = _sigv4("PUT", key, payload, cred, content_type)
|
|
hdrs = _sigv4("PUT", key, payload, cred, content_type)
|
|
|
if not public_read:
|
|
if not public_read:
|
|
|
hdrs.pop("x-amz-acl", None)
|
|
hdrs.pop("x-amz-acl", None)
|
|
|
- url = f"https://{S3_BUCKET}.{S3_HOST}/{key}"
|
|
|
|
|
|
|
+ url = f"https://{S3_UPLOAD_HOST}/{key}"
|
|
|
req = urllib.request.Request(url, data=payload, method="PUT", headers=hdrs)
|
|
req = urllib.request.Request(url, data=payload, method="PUT", headers=hdrs)
|
|
|
with urllib.request.urlopen(req, timeout=300) as r:
|
|
with urllib.request.urlopen(req, timeout=300) as r:
|
|
|
if r.status not in (200, 201):
|
|
if r.status not in (200, 201):
|
|
@@ -351,10 +356,11 @@ def s3_put_file(path: str, key: str, content_type: str = None) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
def public_url(key: str) -> str:
|
|
def public_url(key: str) -> str:
|
|
|
- """公开访问 URL。s3.fmode.cn CNAME 生效前用 OBS 原生域名。"""
|
|
|
|
|
|
|
+ """公开访问 URL 铁律: 一律 https://s3.fmode.cn/<key>(CDN 静态渲染, 全小写 Host)。
|
|
|
|
|
+ S3_PUBLIC_HOST 可覆盖; 旧 S3_PUBLIC_BASE 环境变量保持兼容。"""
|
|
|
if os.environ.get("S3_PUBLIC_BASE"):
|
|
if os.environ.get("S3_PUBLIC_BASE"):
|
|
|
return f"{os.environ['S3_PUBLIC_BASE'].rstrip('/')}/{key}"
|
|
return f"{os.environ['S3_PUBLIC_BASE'].rstrip('/')}/{key}"
|
|
|
- return f"https://{S3_BUCKET}.{S3_HOST}/{key}"
|
|
|
|
|
|
|
+ return f"https://{S3_PUBLIC_HOST}/{key}"
|
|
|
|
|
|
|
|
|
|
|
|
|
def guess_content_type(path: str) -> str:
|
|
def guess_content_type(path: str) -> str:
|