"""SeaweedFS S3-compatible uploads (server: s3.nomadro.com → 127.0.0.1:8333).""" from __future__ import annotations import mimetypes import uuid from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from urllib.parse import quote from fastapi import HTTPException, UploadFile from app.config import settings @dataclass class UploadResult: url: str object_key: str size: int content_type: str def s3_enabled() -> bool: return bool(settings.s3_enabled and settings.s3_endpoint and settings.s3_bucket) def _client(): import boto3 from botocore.client import Config return boto3.client( "s3", endpoint_url=settings.s3_endpoint.rstrip("/"), aws_access_key_id=settings.s3_access_key, aws_secret_access_key=settings.s3_secret_key, region_name=settings.s3_region, config=Config(signature_version="s3v4", s3={"addressing_style": "path"}), ) def ensure_bucket() -> None: if not s3_enabled(): return client = _client() bucket = settings.s3_bucket try: client.head_bucket(Bucket=bucket) except Exception: try: client.create_bucket(Bucket=bucket) except Exception: pass def public_url_for_key(key: str) -> str: base = (settings.s3_public_url or settings.s3_endpoint).rstrip("/") bucket = settings.s3_bucket.strip("/") return f"{base}/{bucket}/{quote(key, safe='/')}" def _object_key(filename: str, purpose: str) -> str: suffix = Path(filename or "upload.bin").suffix.lower() or ".bin" day = datetime.now(timezone.utc).strftime("%Y/%m/%d") prefix = (settings.s3_upload_prefix or "nomadweb").strip("/") purpose_part = (purpose or "uploads").replace("/", "-")[:40] return f"{prefix}/{purpose_part}/{day}/{uuid.uuid4().hex}{suffix}" async def upload_file(file: UploadFile, *, purpose: str, max_size: int) -> UploadResult: if not s3_enabled(): raise HTTPException(status_code=503, detail="S3 存储未配置") data = await file.read() if len(data) > max_size: raise HTTPException(status_code=400, detail=f"文件不能超过 {max_size // 1024 // 1024}MB") if not data: raise HTTPException(status_code=400, detail="空文件") ensure_bucket() key = _object_key(file.filename or "upload.bin", purpose) ctype = file.content_type or mimetypes.guess_type(file.filename or "")[0] or "application/octet-stream" client = _client() try: client.put_object( Bucket=settings.s3_bucket, Key=key, Body=data, ContentType=ctype, ACL="public-read", ) except Exception: client.put_object(Bucket=settings.s3_bucket, Key=key, Body=data, ContentType=ctype) url = public_url_for_key(key) return UploadResult(url=url, object_key=key, size=len(data), content_type=ctype)