# -*- coding: utf-8 -*- import oss2 import random from datetime import datetime from app.utils.time_util import TimeUtil from fastapi import UploadFile from pathlib import Path from urllib.parse import urljoin from app.config.setting import settings from app.core.exceptions import CustomException from app.core.logger import log class OSSUtil: """ 阿里云OSS上传工具类 """ def __init__(self): """初始化OSS客户端""" try: # 创建Bucket对象,所有Object相关的接口都可以通过Bucket对象来进行 auth = oss2.Auth(settings.OSS_ACCESS_KEY_ID, settings.OSS_ACCESS_KEY_SECRET) self.bucket = oss2.Bucket(auth, settings.OSS_ENDPOINT, settings.OSS_BUCKET_NAME) log.info("OSS客户端初始化成功") except Exception as e: log.error(f"OSS客户端初始化失败: {e}") raise CustomException(msg="OSS服务初始化失败") @staticmethod def generate_random_number() -> str: """ 生成3位随机数字字符串。 返回: - str: 三位随机数字字符串。 """ return f'{random.randint(1, 999):03}' @staticmethod def check_file_extension(file: UploadFile) -> bool: """ 检查文件后缀是否合法。 参数: - file (UploadFile): 上传的文件对象。 返回: - bool: 文件后缀是否合法。 异常: - CustomException: 文件类型不支持时抛出。 """ if file.content_type and file.filename: # 优先使用文件名的扩展名 file_extension = '.' + file.filename.rsplit('.', 1)[-1].lower() if '.' in file.filename else None if file_extension and file_extension in settings.ALLOWED_EXTENSIONS: return True raise CustomException(msg="文件类型不支持") else: raise CustomException(msg="文件类型不支持") @staticmethod def check_file_size(file: UploadFile) -> bool: """ 校验文件大小是否合法。 参数: - file (UploadFile): 上传的文件对象。 返回: - bool: 文件大小是否合法(未提供 size 返回 False)。 """ if file.size: return file.size <= settings.MAX_FILE_SIZE else: return False @classmethod def generate_file_name(cls, filename: str) -> str: """ 生成文件名称。 参数: - filename (str): 原始文件名(包含拓展名)。 返回: - str: 生成的文件名(包含时间戳、机器码、随机码)。 """ name, ext = filename.rsplit(".", 1) timestamp = TimeUtil.now_u8().strftime("%Y%m%d%H%M%S") return f'{name}_{timestamp}{settings.UPLOAD_MACHINE}{cls.generate_random_number()}.{ext}' @classmethod def generate_oss_key(cls, filename: str) -> str: """ 生成OSS对象键(路径) 参数: - filename (str): 文件名 返回: - str: OSS对象键,格式如: upload/2026/02/08/filename.jpg """ date_path = TimeUtil.now_u8().strftime("%Y/%m/%d") return f"upload/{date_path}/{filename}" # 上传文件至OSS方法 async def upload_file(self, file: UploadFile) -> tuple[str, str, str]: """ 上传文件到阿里云OSS 参数: - file (UploadFile): 上传的文件对象。 返回: - tuple[str, str, str]: (文件名, OSS对象键, 文件访问URL)。 异常: - CustomException: 当文件类型不支持或大小超限时抛出。 """ # 文件校验(校验文件大小、校验文件后缀) if not all([self.check_file_extension(file), self.check_file_size(file)]): raise CustomException(msg='文件类型或大小不合法') try: # 生成文件名 if not file.filename: raise CustomException(msg='文件名不能为空') # 生成文件名称 filename = self.generate_file_name(file.filename) # 生成oss文件路径(格式如: upload/2026/02/08/filename.jpg) oss_key = self.generate_oss_key(filename) # 读取文件内容 file_content = await file.read() # 上传到OSS result = self.bucket.put_object(oss_key, file_content) if result.status == 200: # 生成访问URL(OSS访问域名/文件路径信息) file_url = f"{settings.OSS_DOMAIN}/{oss_key}" log.info(f"文件上传OSS成功: {oss_key}") return filename, oss_key, file_url else: log.error(f"OSS上传失败,状态码: {result.status}") raise CustomException(msg='文件上传失败') except oss2.exceptions.OssError as e: log.error(f"OSS上传异常: {e}") raise CustomException(msg=f'OSS上传失败: {e}') except Exception as e: log.error(f"文件上传失败: {e}") raise CustomException(msg='文件上传失败') def delete_file(self, oss_key: str) -> bool: """ 删除OSS中的文件 参数: - oss_key (str): OSS对象键 返回: - bool: 删除是否成功 """ try: result = self.bucket.delete_object(oss_key) if result.status == 204: log.info(f"OSS文件删除成功: {oss_key}") return True else: log.error(f"OSS文件删除失败,状态码: {result.status}") return False except oss2.exceptions.OssError as e: log.error(f"OSS删除异常: {e}") return False except Exception as e: log.error(f"文件删除失败: {e}") return False def get_file_url(self, oss_key: str) -> str: """ 获取文件访问URL 参数: - oss_key (str): OSS对象键 返回: - str: 文件访问URL """ return f"{settings.OSS_DOMAIN}/{oss_key}" def file_exists(self, oss_key: str) -> bool: """ 检查文件是否存在 参数: - oss_key (str): OSS对象键 返回: - bool: 文件是否存在 """ try: return self.bucket.object_exists(oss_key) except Exception as e: log.error(f"检查文件存在性失败: {e}") return False