# app/routers/upload.py import os import uuid from datetime import datetime from pathlib import Path from PIL import Image as PILImage from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form from pydantic import BaseModel from app.config import get_settings from app.models.admin import Admin from app.auth import get_current_admin router = APIRouter() settings = get_settings() # Allowed file types (MIME) ALLOWED_IMAGE_TYPES = { "image/jpeg", "image/png", "image/gif", "image/webp", "image/x-icon", "image/vnd.microsoft.icon", # .ico favicon "image/svg+xml", # .svg } # Allowed extensions (fallback when browser reports wrong MIME) ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".ico", ".svg"} MAX_FILE_SIZE = settings.MAX_UPLOAD_SIZE # 5MB # Schemas class UploadResponse(BaseModel): code: int = 200 data: dict = None message: str = "success" def create_thumbnail(image_path: Path, thumb_path: Path, max_size: int = 300): """创建缩略图""" try: with PILImage.open(image_path) as img: # 转换为 RGB(处理透明图片) if img.mode in ('RGBA', 'P'): img = img.convert('RGB') # 等比缩放到最大尺寸 img.thumbnail((max_size, max_size), PILImage.Resampling.LANCZOS) # 保存 img.save(thumb_path, "JPEG", quality=85) return True except Exception as e: print(f"创建缩略图失败: {e}") return False @router.post("/upload/image", response_model=UploadResponse) async def upload_image( file: UploadFile = File(...), subdir: str = Form(default=""), current_admin = Depends(get_current_admin) ): """Upload image file - 支持按子目录存储""" # Check file type - allow by MIME or by extension (browsers may report wrong MIME for .ico) file_ext = os.path.splitext(file.filename or "")[1].lower() if file.content_type not in ALLOWED_IMAGE_TYPES and file_ext not in ALLOWED_EXTENSIONS: raise HTTPException( status_code=400, detail=f"不支持的文件类型: {file.content_type} ({file_ext})" ) # Check file size content = await file.read() if len(content) > MAX_FILE_SIZE: raise HTTPException( status_code=400, detail=f"文件大小超过限制: {MAX_FILE_SIZE / 1024 / 1024}MB" ) # Generate unique filename with timestamp ext = os.path.splitext(file.filename)[1].lower() if not ext: ext = ".jpg" if file.content_type == "image/jpeg" else ".png" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"{timestamp}_{uuid.uuid4().hex[:8]}{ext}" # 根据子目录存储 upload_path = Path(settings.UPLOAD_DIR) if subdir: upload_path = upload_path / subdir upload_path.mkdir(parents=True, exist_ok=True) # 缩略图目录(始终放在 thumbnails 下) thumbnail_dir = Path(settings.UPLOAD_DIR) / "thumbnails" thumbnail_dir.mkdir(exist_ok=True) # Save file file_path = upload_path / filename with open(file_path, "wb") as f: f.write(content) # 创建缩略图(.ico 和 .svg 跳过缩略图生成) thumbnail_name = f"{Path(filename).stem}.jpg" thumb_path = thumbnail_dir / thumbnail_name thumb_relative_path = None if ext not in ('.ico', '.svg') and create_thumbnail(file_path, thumb_path): thumb_relative_path = f"/storage/admin/images/thumbnails/{thumbnail_name}" # Return URL if subdir: url = f"/storage/admin/images/{subdir}/{filename}" else: url = f"/storage/admin/images/{filename}" return UploadResponse( message="上传成功", data={ "url": url, "filename": filename, "size": len(content), "thumb_url": thumb_relative_path } )