74d50c829d
Co-Authored-By: Claude <noreply@anthropic.com>
70 lines
2.8 KiB
Python
70 lines
2.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""媒体上传(图片/pdf/doc 等)—— OSS 优先(OSS_ENDPOINT 配置后走桶),否则落 serverdata/uploads。
|
||
|
||
统一返回契约:绝对 URL。编辑器图片/视频/封面、C端资料上传共用。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import secrets
|
||
import time
|
||
from pathlib import Path
|
||
|
||
from fastapi import HTTPException, UploadFile
|
||
from .. import config
|
||
|
||
# server-data 上传目录(server-core 根下 serverdata/uploads,本地降级用)
|
||
SERVER_CORE_DIR = Path(__file__).resolve().parents[2]
|
||
UPLOAD_DIR = SERVER_CORE_DIR / "serverdata" / "uploads"
|
||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 站点公开基础地址(把 /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 或空则原样返回。"""
|
||
if not u:
|
||
return u or ""
|
||
u = str(u).strip()
|
||
if u.startswith("http://") or u.startswith("https://"):
|
||
return u
|
||
return PUBLIC_BASE + u
|
||
|
||
|
||
def _oss_url(key: str) -> str:
|
||
"""OSS 对象 -> 可访问绝对 URL(公共读桶 endpoint/bucket/key)。"""
|
||
endpoint = (config.OSS_ENDPOINT or "").rstrip("/")
|
||
return f"{endpoint}/{config.OSS_BUCKET}/{key}"
|
||
|
||
|
||
async def save_media(file: UploadFile) -> str:
|
||
"""保存上传文件:OSS 优先,否则本地 uploads;返回绝对 URL;不合法抛 400/413。"""
|
||
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 限制")
|
||
fname = f"up_{int(time.time())}_{secrets.token_hex(2)}{ext}"
|
||
|
||
if config.OSS_ENDPOINT and config.OSS_SECRET_KEY and config.OSS_ACCESS_KEY:
|
||
from ..infrastructure.oss import OSS
|
||
path = await OSS().upload(fname, payload, content_type=_MIME.get(ext, "application/octet-stream"))
|
||
# upload 返回 /oss/<key> 或 /uploads/<key>——OSS 桶走 endpoint/bucket/key
|
||
return _oss_url(fname)
|
||
with open(UPLOAD_DIR / fname, "wb") as f:
|
||
f.write(payload)
|
||
return abs_url(f"/uploads/{fname}")
|