Files
LoveACE-EndF/utils/s3_client.py
2025-08-03 16:50:56 +08:00

370 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
from typing import Optional, Dict, Any, BinaryIO, Union
from pathlib import Path
from loguru import logger
from config import config_manager
# 可选导入aioboto3
try:
import aioboto3
from botocore.exceptions import ClientError, NoCredentialsError
HAS_BOTO3 = True
except ImportError:
aioboto3 = None
ClientError = Exception
NoCredentialsError = Exception
HAS_BOTO3 = False
class S3Client:
"""异步S3客户端"""
def __init__(self):
self._session = None
self._client = None
self._config = None
if not HAS_BOTO3:
logger.warning("aioboto3未安装S3客户端功能不可用")
def _get_s3_config(self):
"""获取S3配置"""
if self._config is None:
self._config = config_manager.get_settings().s3
return self._config
async def _get_client(self):
"""获取S3客户端"""
if not HAS_BOTO3:
raise RuntimeError("aioboto3未安装无法使用S3客户端功能。请运行: pip install aioboto3")
if self._client is None:
config = self._get_s3_config()
# 验证必要的配置
if not config.access_key_id or not config.secret_access_key:
raise ValueError("S3 access_key_id 和 secret_access_key 不能为空")
if not config.bucket_name:
raise ValueError("S3 bucket_name 不能为空")
if self._session is None:
self._session = aioboto3.Session()
self._client = self._session.client(
's3',
aws_access_key_id=config.access_key_id,
aws_secret_access_key=config.secret_access_key,
endpoint_url=config.endpoint_url,
region_name=config.region_name,
use_ssl=config.use_ssl,
config=aioboto3.Config(signature_version=config.signature_version)
)
return self._client
async def upload_file(
self,
file_path: Union[str, Path],
key: str,
bucket: Optional[str] = None,
extra_args: Optional[Dict[str, Any]] = None
) -> bool:
"""
上传文件到S3
Args:
file_path: 本地文件路径
key: S3对象键名
bucket: 存储桶名称如果为None则使用配置中的默认bucket
extra_args: 额外参数如metadata, ACL等
Returns:
bool: 是否上传成功
"""
try:
config = self._get_s3_config()
bucket = bucket or config.bucket_name
if not bucket:
raise ValueError("bucket名称不能为空")
async with await self._get_client() as s3:
await s3.upload_file(
Filename=str(file_path),
Bucket=bucket,
Key=key,
ExtraArgs=extra_args or {}
)
logger.info(f"文件上传成功: {file_path} -> s3://{bucket}/{key}")
return True
except FileNotFoundError:
logger.error(f"文件不存在: {file_path}")
return False
except NoCredentialsError:
logger.error("S3凭据未配置或无效")
return False
except ClientError as e:
logger.error(f"S3客户端错误: {e}")
return False
except Exception as e:
logger.error(f"上传文件失败: {e}")
return False
async def download_file(
self,
key: str,
file_path: Union[str, Path],
bucket: Optional[str] = None
) -> bool:
"""
从S3下载文件
Args:
key: S3对象键名
file_path: 本地保存路径
bucket: 存储桶名称如果为None则使用配置中的默认bucket
Returns:
bool: 是否下载成功
"""
try:
config = self._get_s3_config()
bucket = bucket or config.bucket_name
if not bucket:
raise ValueError("bucket名称不能为空")
# 确保目标目录存在
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
async with await self._get_client() as s3:
await s3.download_file(
Bucket=bucket,
Key=key,
Filename=str(file_path)
)
logger.info(f"文件下载成功: s3://{bucket}/{key} -> {file_path}")
return True
except ClientError as e:
if e.response['Error']['Code'] == 'NoSuchKey':
logger.error(f"S3对象不存在: s3://{bucket}/{key}")
else:
logger.error(f"S3客户端错误: {e}")
return False
except Exception as e:
logger.error(f"下载文件失败: {e}")
return False
async def upload_bytes(
self,
data: bytes,
key: str,
bucket: Optional[str] = None,
content_type: Optional[str] = None,
extra_args: Optional[Dict[str, Any]] = None
) -> bool:
"""
上传字节数据到S3
Args:
data: 要上传的字节数据
key: S3对象键名
bucket: 存储桶名称如果为None则使用配置中的默认bucket
content_type: 内容类型
extra_args: 额外参数
Returns:
bool: 是否上传成功
"""
try:
config = self._get_s3_config()
bucket = bucket or config.bucket_name
if not bucket:
raise ValueError("bucket名称不能为空")
extra_args = extra_args or {}
if content_type:
extra_args['ContentType'] = content_type
async with await self._get_client() as s3:
await s3.put_object(
Bucket=bucket,
Key=key,
Body=data,
**extra_args
)
logger.info(f"数据上传成功: s3://{bucket}/{key}")
return True
except Exception as e:
logger.error(f"上传数据失败: {e}")
return False
async def download_bytes(
self,
key: str,
bucket: Optional[str] = None
) -> Optional[bytes]:
"""
从S3下载字节数据
Args:
key: S3对象键名
bucket: 存储桶名称如果为None则使用配置中的默认bucket
Returns:
Optional[bytes]: 下载的字节数据失败时返回None
"""
try:
config = self._get_s3_config()
bucket = bucket or config.bucket_name
if not bucket:
raise ValueError("bucket名称不能为空")
async with await self._get_client() as s3:
response = await s3.get_object(Bucket=bucket, Key=key)
async with response['Body'] as stream:
data = await stream.read()
logger.info(f"数据下载成功: s3://{bucket}/{key}")
return data
except ClientError as e:
if e.response['Error']['Code'] == 'NoSuchKey':
logger.error(f"S3对象不存在: s3://{bucket}/{key}")
else:
logger.error(f"S3客户端错误: {e}")
return None
except Exception as e:
logger.error(f"下载数据失败: {e}")
return None
async def delete_object(
self,
key: str,
bucket: Optional[str] = None
) -> bool:
"""
删除S3对象
Args:
key: S3对象键名
bucket: 存储桶名称如果为None则使用配置中的默认bucket
Returns:
bool: 是否删除成功
"""
try:
config = self._get_s3_config()
bucket = bucket or config.bucket_name
if not bucket:
raise ValueError("bucket名称不能为空")
async with await self._get_client() as s3:
await s3.delete_object(Bucket=bucket, Key=key)
logger.info(f"对象删除成功: s3://{bucket}/{key}")
return True
except Exception as e:
logger.error(f"删除对象失败: {e}")
return False
async def list_objects(
self,
prefix: str = "",
bucket: Optional[str] = None,
max_keys: int = 1000
) -> list:
"""
列出S3对象
Args:
prefix: 对象键前缀
bucket: 存储桶名称如果为None则使用配置中的默认bucket
max_keys: 最大返回对象数量
Returns:
list: 对象列表
"""
try:
config = self._get_s3_config()
bucket = bucket or config.bucket_name
if not bucket:
raise ValueError("bucket名称不能为空")
async with await self._get_client() as s3:
response = await s3.list_objects_v2(
Bucket=bucket,
Prefix=prefix,
MaxKeys=max_keys
)
objects = response.get('Contents', [])
logger.info(f"列出对象成功: s3://{bucket}/{prefix}* ({len(objects)}个对象)")
return objects
except Exception as e:
logger.error(f"列出对象失败: {e}")
return []
async def object_exists(
self,
key: str,
bucket: Optional[str] = None
) -> bool:
"""
检查S3对象是否存在
Args:
key: S3对象键名
bucket: 存储桶名称如果为None则使用配置中的默认bucket
Returns:
bool: 对象是否存在
"""
try:
config = self._get_s3_config()
bucket = bucket or config.bucket_name
if not bucket:
raise ValueError("bucket名称不能为空")
async with await self._get_client() as s3:
await s3.head_object(Bucket=bucket, Key=key)
return True
except ClientError as e:
if e.response['Error']['Code'] == '404':
return False
else:
logger.error(f"检查对象存在性失败: {e}")
return False
except Exception as e:
logger.error(f"检查对象存在性失败: {e}")
return False
async def close(self):
"""关闭S3客户端"""
if self._client:
await self._client.close()
self._client = None
if self._session:
await self._session.close()
self._session = None
# 全局S3客户端实例
s3_client = S3Client()