Files

113 lines
3.7 KiB
Python

"""
对象存储:S3 兼容客户端壳(boto3)。
本文件职责:组装 client、put_bytes / get_bytes;缺依赖或未配齐时明确失败。
禁止:在本包写工单状态;正式登记附件元数据走主账。
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Optional
from agent.config import Settings, get_settings
logger = logging.getLogger(__name__)
@dataclass
class S3ObjectStorage:
"""
基于 boto3 的 S3/MinIO 壳。
allow_network=False(默认):不真正 PUT,只校验配置与 SDK 可导入。
"""
settings: Settings
allow_network: bool = False
@classmethod
def from_settings(
cls,
settings: Optional[Settings] = None,
*,
allow_network: bool = False,
) -> "S3ObjectStorage":
return cls(settings=settings or get_settings(), allow_network=allow_network)
def _cfg(self) -> tuple[str, str, str, str, bool]:
s = self.settings
return (
(s.object_storage_endpoint_url or "").rstrip("/"),
s.object_storage_bucket or "",
s.object_storage_access_key or "",
s.object_storage_secret_key or "",
bool(s.object_storage_path_style),
)
def configured(self) -> bool:
endpoint, bucket, *_rest = self._cfg()
return bool(endpoint and bucket)
def sdk_available(self) -> bool:
try:
import boto3 # noqa: F401
return True
except ImportError:
return False
def _client(self):
"""构造 boto3 client;未配置抛 RuntimeError。"""
import boto3
from botocore.client import Config
endpoint, bucket, ak, sk, path_style = self._cfg()
if not endpoint or not bucket or not ak or not sk:
raise RuntimeError("object_storage_not_configured")
return boto3.client(
"s3",
endpoint_url=endpoint,
aws_access_key_id=ak,
aws_secret_access_key=sk,
config=Config(s3={"addressing_style": "path" if path_style else "auto"}),
)
def put_bytes(self, *, key: str, data: bytes, content_type: str = "") -> dict:
"""
上传字节。
allow_network=False:返回 dry_run,不访问网络。
"""
endpoint, bucket, ak, sk, _ps = self._cfg()
if not endpoint or not bucket:
return {"ok": False, "error": "not_configured"}
if not self.sdk_available():
return {"ok": False, "error": "boto3_not_installed"}
if not self.allow_network:
logger.info(
"storage.s3.put dry_run bucket=%s key=%s bytes=%s",
bucket,
key,
len(data),
)
return {"ok": True, "dry_run": True, "bucket": bucket, "key": key}
extra = {}
if content_type:
extra["ContentType"] = content_type
self._client().put_object(Bucket=bucket, Key=key, Body=data, **extra)
return {"ok": True, "dry_run": False, "bucket": bucket, "key": key}
def get_bytes(self, *, key: str) -> dict:
"""下载壳;默认 dry_run 不拉网。"""
endpoint, bucket, *_r = self._cfg()
if not endpoint or not bucket:
return {"ok": False, "error": "not_configured"}
if not self.allow_network:
return {"ok": True, "dry_run": True, "key": key, "data": b""}
if not self.sdk_available():
return {"ok": False, "error": "boto3_not_installed"}
obj = self._client().get_object(Bucket=bucket, Key=key)
body = obj["Body"].read()
return {"ok": True, "dry_run": False, "key": key, "data": body}