370 lines
11 KiB
Python
370 lines
11 KiB
Python
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() |