985b85cb4e
- infrastructure/oss.py 重写:服务端上传/预签名上下行/CDN鉴权直链(方式A)/resolve_url·to_object_path 出入口规范 - config.py 新增 OSS_REGION/OSS_CDN_BASE_URL/OSS_CDN_AUTH_KEY - media_upload 统一走 oss 单例,返回 CDN 直链;dispatcher /oss 前缀路由 - 管理端用户列表头像出口 CDN 化
66 lines
2.9 KiB
Python
66 lines
2.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""媒体上传(图片/pdf/doc 等)—— 统一走基础设施层 OSS(infrastructure.oss.oss)。
|
||
|
||
OSS 已配置时上传到桶并返回 /oss/<key> 的绝对 URL(下载经 /oss 路由跳预签名);
|
||
未配置时降级本地 serverdata/uploads。编辑器图片/视频/封面、C端资料上传共用。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import secrets
|
||
import time
|
||
from pathlib import Path
|
||
|
||
from fastapi import HTTPException, UploadFile
|
||
from .. import config
|
||
|
||
# server-data 上传目录(本地降级用,与 infrastructure.oss 同一目录)
|
||
UPLOAD_DIR = config.SERVERDATA_DIR / "uploads"
|
||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 站点公开基础地址(把 /oss、/uploads 相对路径补全为完整 HTTPS)
|
||
PUBLIC_BASE = __import__("os").environ.get("PUBLIC_BASE", "https://opc.pinesound.cn").rstrip("/")
|
||
|
||
ALLOWED_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf", ".doc", ".docx"}
|
||
MAX_SIZE = 20 * 1024 * 1024 # 20MB
|
||
|
||
# 扩展名 → content-type
|
||
_MIME = {
|
||
".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png",
|
||
".webp": "image/webp", ".gif": "image/gif",
|
||
".pdf": "application/pdf", ".doc": "application/msword",
|
||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||
}
|
||
|
||
|
||
def abs_url(u: str | None) -> str:
|
||
"""相对路径补全为绝对 URL;已是完整 URL 或空则原样返回(委托基础设施层 resolve_url)。"""
|
||
from ..infrastructure.oss import resolve_url
|
||
return resolve_url(u)
|
||
|
||
|
||
async def save_media(file: UploadFile, dir: str = "misc") -> str:
|
||
"""保存上传文件(OSS 优先,本地降级);返回 CDN/OSS 直链绝对 URL;不合法抛 400/413。
|
||
|
||
OSS 路径规范:<业务目录>/<YYYYMM>/<时间戳>_<随机>.<ext>
|
||
业务目录:avatar 头像 / park-admission 入驻认证资料 / news 资讯 / event 活动封面 / park 大屏媒体 / misc 兜底
|
||
"""
|
||
from ..infrastructure.oss import oss
|
||
|
||
name = (file.filename or "").rsplit("/", 1)[-1]
|
||
ext = ("." + name.split(".")[-1].lower()) if "." in name else ""
|
||
if ext not in ALLOWED_EXT:
|
||
raise HTTPException(status_code=400, detail=f"不支持的文件类型({ext or '无扩展名'})")
|
||
payload = await file.read()
|
||
if len(payload) > MAX_SIZE:
|
||
raise HTTPException(status_code=413, detail="文件超过 20MB 限制")
|
||
key = build_key(dir, ext)
|
||
await oss.upload(key, payload, content_type=_MIME.get(ext, "application/octet-stream"))
|
||
# 直接返回 CDN/OSS 直链,不经服务端代理
|
||
return abs_url(oss.direct_url(key))
|
||
|
||
|
||
def build_key(dir: str, ext: str) -> str:
|
||
"""按规范生成对象 key:<业务目录>/<YYYYMM>/<时间戳>_<随机>.<ext>。"""
|
||
d = "".join(c for c in (dir or "misc").lower() if c.isalnum() or c in "-_") or "misc"
|
||
return f"{d}/{time.strftime('%Y%m')}/{int(time.time())}_{secrets.token_hex(4)}{ext}"
|