""" 阿里云 OSS 简单上传工具 参考文档: https://help.aliyun.com/zh/oss/user-guide/simple-upload 支持: - 上传本地文件 - 上传字节数据 / 字符串 - 上传文件流(file-like object) - 自动根据文件后缀生成 OSS 对象路径 - 返回可公开访问的 URL(需确保 Bucket 已开启公共读或已配置签名) 前置条件: 1. pip install alibabacloud-oss-v2 2. 在 .env 中配置以下变量: OSS_ACCESS_KEY_ID=<你的 AccessKey ID> OSS_ACCESS_KEY_SECRET=<你的 AccessKey Secret> OSS_BUCKET_NAME=<存储空间名称> OSS_ENDPOINT= OSS_REGION=<地域,如 cn-hangzhou> OSS_URL_PREFIX=<可选,自定义域名前缀,如 https://cdn.example.com> """ import os import uuid import mimetypes from datetime import datetime from pathlib import Path from typing import Optional, Union, BinaryIO import alibabacloud_oss_v2 as oss from dotenv import load_dotenv # ── 加载环境变量 ────────────────────────────────────────────── load_dotenv() # AccessKey 从系统环境变量读取(~/.bashrc 中 export 设置) OSS_ACCESS_KEY_ID = os.environ.get("OSS_ACCESS_KEY_ID", "") OSS_ACCESS_KEY_SECRET = os.environ.get("OSS_ACCESS_KEY_SECRET", "") # 以下配置从 .env 文件读取 OSS_BUCKET_NAME = os.getenv("OSS_BUCKET_NAME", "") OSS_ENDPOINT = os.getenv("OSS_ENDPOINT", "") OSS_REGION = os.getenv("OSS_REGION", "") # 可选:自定义域名前缀,用于拼接返回的公开 URL OSS_URL_PREFIX = os.getenv("OSS_URL_PREFIX", "") def _get_client() -> oss.Client: """创建并返回 OSS 客户端实例""" credentials_provider = oss.credentials.StaticCredentialsProvider( access_key_id=OSS_ACCESS_KEY_ID, access_key_secret=OSS_ACCESS_KEY_SECRET, ) cfg = oss.config.load_default() cfg.credentials_provider = credentials_provider cfg.region = OSS_REGION cfg.endpoint = OSS_ENDPOINT return oss.Client(cfg) def _generate_object_key(filename: str, prefix: str = "uploads") -> str: """ 根据文件名生成唯一的 OSS 对象 Key 格式: {prefix}/{日期}/{uuid}_{原始文件名} """ date_str = datetime.now().strftime("%Y%m%d") unique_id = uuid.uuid4().hex[:8] safe_name = Path(filename).name # 只取文件名,去掉路径 return f"{prefix}/{date_str}/{unique_id}_{safe_name}" def _build_url(object_key: str) -> str: """根据对象 Key 构建可访问的 URL""" if OSS_URL_PREFIX: return f"{OSS_URL_PREFIX.rstrip('/')}/{object_key}" # 默认使用 Bucket 域名拼接 endpoint = OSS_ENDPOINT.replace("https://", "").replace("http://", "") return f"https://{OSS_BUCKET_NAME}.{endpoint}/{object_key}" def upload_file( file_path: str, object_key: Optional[str] = None, prefix: str = "uploads", ) -> dict: """ 上传本地文件到 OSS 参数: file_path: 本地文件的绝对路径 object_key: 自定义 OSS 对象名称,为 None 则自动生成 prefix: 对象 Key 的前缀目录 返回: 包含上传结果的字典: { "url": "文件访问地址", "object_key": "OSS 对象路径", "etag": "ETag", "status_code": 200, } """ if not os.path.isfile(file_path): raise FileNotFoundError(f"文件不存在: {file_path}") filename = os.path.basename(file_path) if object_key is None: object_key = _generate_object_key(filename, prefix) # 检测文件类型,设置 Content-Type content_type, _ = mimetypes.guess_type(file_path) client = _get_client() result = client.put_object_from_file( oss.PutObjectRequest( bucket=OSS_BUCKET_NAME, key=object_key, content_type=content_type, ), file_path, ) return { "url": _build_url(object_key), "object_key": object_key, "etag": result.etag, "status_code": result.status_code, } def upload_bytes( data: Union[bytes, str], filename: str, object_key: Optional[str] = None, prefix: str = "uploads", content_type: Optional[str] = None, ) -> dict: """ 上传字节数据或字符串到 OSS 参数: data: 要上传的数据(bytes 或 str) filename: 用于生成 Key 的文件名(如 "report.txt") object_key: 自定义 OSS 对象名称,为 None 则自动生成 prefix: 对象 Key 的前缀目录 content_type: 自定义 Content-Type 返回: 包含上传结果的字典 """ if isinstance(data, str): data = data.encode("utf-8") if object_key is None: object_key = _generate_object_key(filename, prefix) if content_type is None: content_type, _ = mimetypes.guess_type(filename) client = _get_client() result = client.put_object( oss.PutObjectRequest( bucket=OSS_BUCKET_NAME, key=object_key, body=data, content_type=content_type, ) ) return { "url": _build_url(object_key), "object_key": object_key, "etag": result.etag, "status_code": result.status_code, } def upload_fileobj( fileobj: BinaryIO, filename: str, object_key: Optional[str] = None, prefix: str = "uploads", content_type: Optional[str] = None, ) -> dict: """ 上传文件流(file-like object)到 OSS 参数: fileobj: 文件流对象(如 open(path, 'rb') 或 FastAPI 的 UploadFile.file) filename: 用于生成 Key 的文件名 object_key: 自定义 OSS 对象名称,为 None 则自动生成 prefix: 对象 Key 的前缀目录 content_type: 自定义 Content-Type 返回: 包含上传结果的字典 """ data = fileobj.read() return upload_bytes( data=data, filename=filename, object_key=object_key, prefix=prefix, content_type=content_type, ) # ──────────────────────────────────────────────────────────────── # 命令行入口:python -m utils.oss_uploader --file <路径> # ──────────────────────────────────────────────────────────────── if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="阿里云 OSS 简单上传工具") parser.add_argument("--file", required=True, help="要上传的本地文件路径") parser.add_argument("--key", default=None, help="自定义 OSS 对象路径(可选)") parser.add_argument( "--prefix", default="uploads", help="对象 Key 前缀(默认: uploads)" ) args = parser.parse_args() result = upload_file(args.file, object_key=args.key, prefix=args.prefix) print("✅ 上传成功!") print(f" 访问地址: {result['url']}") print(f" 对象路径: {result['object_key']}") print(f" ETag: {result['etag']}") print(f" 状态码: {result['status_code']}")