149 lines
5.0 KiB
Python
149 lines
5.0 KiB
Python
"""
|
||
阿里云 OSS 上传工具
|
||
"""
|
||
import os
|
||
import uuid
|
||
from typing import Optional
|
||
import oss2
|
||
from .config import settings
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
def get_oss_bucket():
|
||
"""获取 OSS bucket 实例"""
|
||
if not all([
|
||
settings.ALIYUN_OSS_ACCESS_KEY_ID,
|
||
settings.ALIYUN_OSS_ACCESS_KEY_SECRET,
|
||
settings.ALIYUN_OSS_BUCKET_NAME,
|
||
settings.ALIYUN_OSS_ENDPOINT
|
||
]):
|
||
raise ValueError("OSS 配置不完整")
|
||
|
||
auth = oss2.Auth(
|
||
settings.ALIYUN_OSS_ACCESS_KEY_ID,
|
||
settings.ALIYUN_OSS_ACCESS_KEY_SECRET
|
||
)
|
||
|
||
bucket = oss2.Bucket(
|
||
auth,
|
||
settings.ALIYUN_OSS_ENDPOINT,
|
||
settings.ALIYUN_OSS_BUCKET_NAME
|
||
)
|
||
|
||
return bucket
|
||
|
||
def test_oss_connection() -> bool:
|
||
"""测试 OSS 连接是否正常"""
|
||
try:
|
||
logger.info(f"测试 OSS 连接...")
|
||
logger.info(f"Bucket: {settings.ALIYUN_OSS_BUCKET_NAME}")
|
||
logger.info(f"Endpoint: {settings.ALIYUN_OSS_ENDPOINT}")
|
||
logger.info(f"AccessKeyId: {settings.ALIYUN_OSS_ACCESS_KEY_ID[:8]}***")
|
||
|
||
bucket = get_oss_bucket()
|
||
|
||
# 尝试列出 bucket 中的对象(限制1个)
|
||
result = bucket.list_objects(max_keys=1)
|
||
logger.info(f"OSS 连接测试成功,bucket: {settings.ALIYUN_OSS_BUCKET_NAME}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"OSS 连接测试失败: {e}")
|
||
logger.error(f"错误类型: {type(e)}")
|
||
|
||
# 检查是否是权限问题
|
||
error_str = str(e)
|
||
if "AccessDenied" in error_str:
|
||
logger.error("权限被拒绝 - 可能的原因:")
|
||
logger.error("1. AccessKey 没有该 Bucket 的访问权限")
|
||
logger.error("2. Bucket 不存在或属于其他账户")
|
||
logger.error("3. AccessKey 已过期或被禁用")
|
||
elif "NoSuchBucket" in error_str:
|
||
logger.error("Bucket 不存在 - 请检查 Bucket 名称是否正确")
|
||
elif "InvalidAccessKeyId" in error_str:
|
||
logger.error("AccessKey 无效 - 请检查 AccessKey 是否正确")
|
||
elif "SignatureDoesNotMatch" in error_str:
|
||
logger.error("签名不匹配 - 请检查 AccessKeySecret 是否正确")
|
||
|
||
return False
|
||
|
||
def upload_audio_file(audio_data: bytes, file_extension: str = "wav") -> str:
|
||
"""
|
||
上传音频文件到 OSS
|
||
|
||
Args:
|
||
audio_data: 音频二进制数据
|
||
file_extension: 文件扩展名(不含点)
|
||
|
||
Returns:
|
||
公网可访问的文件 URL
|
||
"""
|
||
try:
|
||
bucket = get_oss_bucket()
|
||
|
||
# 生成唯一文件名
|
||
file_id = str(uuid.uuid4())
|
||
object_key = f"voice_call/{file_id}.{file_extension}"
|
||
|
||
# 上传文件
|
||
result = bucket.put_object(object_key, audio_data)
|
||
|
||
if result.status == 200:
|
||
# 构建公网访问 URL
|
||
if settings.ALIYUN_OSS_CDN_DOMAIN:
|
||
# 使用 CDN 域名
|
||
file_url = f"{settings.ALIYUN_OSS_CDN_DOMAIN.rstrip('/')}/{object_key}"
|
||
else:
|
||
# 使用默认域名 - 修复 URL 格式
|
||
endpoint_clean = settings.ALIYUN_OSS_ENDPOINT.replace('https://', '').replace('http://', '').rstrip('/')
|
||
file_url = f"https://{settings.ALIYUN_OSS_BUCKET_NAME}.{endpoint_clean}/{object_key}"
|
||
|
||
logger.info(f"文件上传成功: {object_key} -> {file_url}")
|
||
|
||
# 验证 URL 格式
|
||
if not file_url.startswith('https://'):
|
||
logger.error(f"URL 格式错误: {file_url}")
|
||
raise Exception(f"生成的 URL 格式不正确: {file_url}")
|
||
|
||
return file_url
|
||
else:
|
||
raise Exception(f"上传失败,状态码: {result.status}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"OSS 上传失败: {e}")
|
||
raise
|
||
|
||
def delete_audio_file(file_url: str) -> bool:
|
||
"""
|
||
删除 OSS 上的音频文件
|
||
|
||
Args:
|
||
file_url: 文件的公网 URL
|
||
|
||
Returns:
|
||
是否删除成功
|
||
"""
|
||
try:
|
||
bucket = get_oss_bucket()
|
||
|
||
# 从 URL 提取 object_key
|
||
if settings.ALIYUN_OSS_CDN_DOMAIN and file_url.startswith(settings.ALIYUN_OSS_CDN_DOMAIN):
|
||
object_key = file_url.replace(settings.ALIYUN_OSS_CDN_DOMAIN.rstrip('/') + '/', '')
|
||
else:
|
||
# 从默认域名提取
|
||
domain_prefix = f"https://{settings.ALIYUN_OSS_BUCKET_NAME}.{settings.ALIYUN_OSS_ENDPOINT.replace('https://', '')}/"
|
||
if file_url.startswith(domain_prefix):
|
||
object_key = file_url.replace(domain_prefix, '')
|
||
else:
|
||
logger.warning(f"无法解析文件 URL: {file_url}")
|
||
return False
|
||
|
||
# 删除文件
|
||
result = bucket.delete_object(object_key)
|
||
logger.info(f"文件删除成功: {object_key}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"OSS 删除失败: {e}")
|
||
return False |