# -*- coding: utf-8 -*- """基础设施层 · OSS 对象存储异步封装(aioboto3,兼容阿里云 OSS / MinIO)。 未配置 OSS 时降级为本地上传目录(uploads/),保证无 OSS 也可用。 """ from __future__ import annotations import os from pathlib import Path from typing import Optional from .. import config class OSS: """对象存储:上传文件 / 生成访问 URL。配置了 OSS_ENDPOINT 才启用,否则落本地上传目录。""" def __init__(self) -> None: self._client: Optional[object] = None self.enabled = bool(config.OSS_ENDPOINT) self.bucket = config.OSS_BUCKET self.local_dir = Path(__file__).resolve().parent.parent.parent / "uploads" self.local_dir.mkdir(parents=True, exist_ok=True) async def _get_client(self): if self._client is None: import aioboto3 session = aioboto3.Session( aws_access_key_id=config.OSS_ACCESS_KEY, aws_secret_access_key=config.OSS_SECRET_KEY, ) self._client = session.client( "s3", endpoint_url=config.OSS_ENDPOINT, region_name="oss-cn-hangzhou", ) return self._client async def upload(self, key: str, data: bytes, content_type: str = "application/octet-stream") -> str: """上传对象,返回可访问的相对 URL。""" if self.enabled: client = await self._get_client() await client.put_object(Bucket=self.bucket, Key=key, Body=data, ContentType=content_type) return f"/oss/{key}" # 本地降级 dest = self.local_dir / key dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes(data) return f"/uploads/{key}" async def presigned_url(self, key: str, expires: int = 3600) -> str: """生成临时访问 URL(OSS 启用时)。""" if self.enabled: client = await self._get_client() return await client.generate_presigned_url( "get_object", Params={"Bucket": self.bucket, "Key": key}, ExpiresIn=expires ) return f"/uploads/{key}" @staticmethod def is_configured() -> bool: return bool(config.OSS_ENDPOINT)