# -*- coding: utf-8 -*- """媒体上传(图片/pdf/doc 等)—— 统一走基础设施层 OSS(infrastructure.oss.oss)。 OSS 已配置时上传到桶并返回 /oss/ 的绝对 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 路径规范:<业务目录>//<时间戳>_<随机>. 业务目录: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:<业务目录>//<时间戳>_<随机>.。""" 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}"