""" 文件上传 API ============ 提供通用文件上传功能,直接上传到七牛云对象存储。 """ import logging import uuid from pathlib import Path import io from fastapi import APIRouter, File, HTTPException, UploadFile from pydantic import BaseModel, Field from app.schemas.common import ApiResponse, success_response from app.services.qiniu_service import get_qiniu_service router = APIRouter(prefix="/upload", tags=["Upload"]) logger = logging.getLogger(__name__) class UploadResponse(BaseModel): """上传响应""" url: str = Field(..., description="七牛云文件 URL") key: str = Field(..., description="七牛云文件 key") size: int = Field(..., description="文件大小(字节)") @router.post("/video", response_model=ApiResponse[UploadResponse]) async def upload_video( file: UploadFile = File(..., description="视频文件"), ): """ 上传视频到七牛云 支持格式:mp4, mov, avi, webm 返回七牛云永久访问 URL。 """ try: # 验证文件格式 allowed_types = { "video/mp4", "video/quicktime", "video/x-msvideo", "video/webm", } content_type = file.content_type or "" # 如果 content_type 为空,尝试从文件名推断 if not content_type: ext = Path(file.filename or "").suffix.lower() ext_to_mime = { ".mp4": "video/mp4", ".mov": "video/quicktime", ".avi": "video/x-msvideo", ".webm": "video/webm", } content_type = ext_to_mime.get(ext, "") if content_type not in allowed_types: raise HTTPException( status_code=400, detail=f"不支持的文件格式: {content_type},请上传 mp4/mov/avi/webm 视频", ) # 读取文件内容 content = await file.read() if not content: raise HTTPException(status_code=400, detail="文件内容为空") # 生成唯一文件名 ext = Path(file.filename or "video.mp4").suffix or ".mp4" unique_name = f"{uuid.uuid4().hex[:16]}{ext}" # 上传到七牛云 qiniu = get_qiniu_service() bucket, domain = qiniu._get_bucket_and_domain("video") key = qiniu.generate_key("video", unique_name) stream = io.BytesIO(content) result = qiniu.upload_stream( stream=stream, key=key, mime_type=content_type or "video/mp4", bucket=bucket, domain=domain, ) url = result.get("url") key = result.get("key") if not url: raise HTTPException(status_code=500, detail="上传到七牛云失败:未返回 URL") logger.info(f"[Upload] 视频上传成功: {url[:80]}..., size={len(content)}") return success_response( data=UploadResponse( url=url, key=key or unique_name, size=len(content), ) ) except HTTPException: raise except Exception as e: logger.error(f"[Upload] 视频上传失败: {e}") raise HTTPException(status_code=500, detail=f"上传失败: {e}") @router.post("/image", response_model=ApiResponse[UploadResponse]) async def upload_image( file: UploadFile = File(..., description="图片文件"), ): """ 上传图片到七牛云 支持格式:jpg, png, gif, webp """ try: allowed_types = { "image/jpeg", "image/png", "image/gif", "image/webp", } content_type = file.content_type or "" if not content_type: ext = Path(file.filename or "").suffix.lower() ext_to_mime = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", ".webp": "image/webp", } content_type = ext_to_mime.get(ext, "") if content_type not in allowed_types: raise HTTPException( status_code=400, detail=f"不支持的图片格式: {content_type}", ) content = await file.read() if not content: raise HTTPException(status_code=400, detail="文件内容为空") ext = Path(file.filename or "image.jpg").suffix or ".jpg" unique_name = f"{uuid.uuid4().hex[:16]}{ext}" qiniu = get_qiniu_service() bucket, domain = qiniu._get_bucket_and_domain("image") key = qiniu.generate_key("image", unique_name) stream = io.BytesIO(content) result = qiniu.upload_stream( stream=stream, key=key, mime_type=content_type or "image/jpeg", bucket=bucket, domain=domain, ) url = result.get("url") key = result.get("key") if not url: raise HTTPException(status_code=500, detail="上传到七牛云失败:未返回 URL") logger.info(f"[Upload] 图片上传成功: {url[:80]}..., size={len(content)}") return success_response( data=UploadResponse( url=url, key=key or unique_name, size=len(content), ) ) except HTTPException: raise except Exception as e: logger.error(f"[Upload] 图片上传失败: {e}") raise HTTPException(status_code=500, detail=f"上传失败: {e}")