feat(upload): 媒体上传优先走 OSS(OSS_ENDPOINT 启用→桶,返回 endpoint/bucket/key 绝对URL),本地降级
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""媒体上传(图片/pdf/doc 等)—— 存 serverdata/uploads,返回绝对 URL。
|
||||
"""媒体上传(图片/pdf/doc 等)—— OSS 优先(OSS_ENDPOINT 配置后走桶),否则落 serverdata/uploads。
|
||||
|
||||
与 app/training/main.py 的 /api/upload 共用同一目录与返回契约 {ok, url}。
|
||||
统一返回契约:绝对 URL。编辑器图片/视频/封面、C端资料上传共用。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,8 +10,9 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from .. import config
|
||||
|
||||
# server-data 上传目录(server-core 根下 serverdata/uploads)
|
||||
# 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)
|
||||
@@ -22,6 +23,14 @@ PUBLIC_BASE = __import__("os").environ.get("PUBLIC_BASE", "https://opc.pinesound
|
||||
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 或空则原样返回。"""
|
||||
@@ -33,8 +42,14 @@ def abs_url(u: str | None) -> str:
|
||||
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:
|
||||
"""保存上传文件到 uploads 目录,返回绝对 URL;不合法抛 400/413。"""
|
||||
"""保存上传文件: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:
|
||||
@@ -43,6 +58,12 @@ async def save_media(file: UploadFile) -> str:
|
||||
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}")
|
||||
|
||||
Reference in New Issue
Block a user