"""对象存储上传工具(MinIO,S3 协议兼容) 原实现基于阿里云 OSS SDK(alibabacloud_oss_v2),现改为 boto3 对接 MinIO。 对外函数名与返回值保持不变,业务代码无需修改。 """ import base64 import io import mimetypes import re import threading import time import boto3 import requests from botocore.config import Config from config import ( region, endpoint, bucket, file_url_pre, bucket_path, accessKeyId, accessKeySecret, ) _client = None _client_lock = threading.Lock() def get_client(): """获取(懒加载)S3 客户端,MinIO 必须使用 path-style 寻址""" global _client if _client is None: with _client_lock: if _client is None: _client = boto3.client( "s3", endpoint_url=endpoint, aws_access_key_id=accessKeyId, aws_secret_access_key=accessKeySecret, region_name=region, config=Config( signature_version="s3v4", s3={"addressing_style": "path"}, # MinIO 必须 retries={"max_attempts": 3, "mode": "standard"}, ), ) return _client def _guess_content_type(key: str) -> str: content_type, _ = mimetypes.guess_type(key) return content_type or "application/octet-stream" def get_presigned_url(key: str, expires: int = 7 * 24 * 3600) -> str: """生成临时访问链接(存储桶未开放匿名读时使用),默认有效 7 天""" return get_client().generate_presigned_url( "get_object", Params={"Bucket": bucket, "Key": key.lstrip("/")}, ExpiresIn=expires, ) def upload_file(file_content: bytes, key: str): """上传字节内容到 MinIO,返回可访问链接""" key = key.lstrip("/") client = get_client() client.put_object( Bucket=bucket, # 存储桶名称 Key=key, # 对象名称 Body=io.BytesIO(file_content) if isinstance(file_content, (bytes, bytearray)) else file_content, ContentType=_guess_content_type(key), ) return file_url_pre + key def upload_data_url(data_url: str, prefix: str = "history", key_hint: str = "") -> str: """ 将 base64 data URL 上传到对象存储,返回图片链接 data_url: data:image/png;base64,xxxx 或 data:image/jpeg;base64,xxxx prefix: 对象 key 前缀 key_hint: 可选后缀避免重名,如 "_0", "_1" """ match = re.match(r'data:image/(\w+);base64,(.+)', data_url) if not match: raise ValueError('无效的 data URL 格式') ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg' file_content = base64.b64decode(match.group(2)) ts = int(time.time() * 1000) key = f"{bucket_path}{prefix}/{ts}{key_hint}.{ext}" return upload_file(file_content, key) def upload_data_urls(data_urls: list, prefix: str = "history") -> list: """批量上传 base64 图片到对象存储,返回图片链接列表""" urls = [] ts = int(time.time() * 1000) for i, data_url in enumerate(data_urls or []): if not data_url or not isinstance(data_url, str): continue if not data_url.startswith("http"): match = re.match(r'data:image/(\w+);base64,(.+)', data_url) if not match: continue ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg' file_content = base64.b64decode(match.group(2)) else: file_content = requests.get(data_url).content ext = "png" key = f"{bucket_path}{prefix}/{ts}_{i}.{ext}" urls.append(upload_file(file_content, key)) return urls # 脚本入口,当文件被直接运行时调用main函数 if __name__ == "__main__": with open("D:\\pack\\nanri\\main.dist\\SHUFU.zip", "rb") as f: file_content = f.read() res = upload_file(file_content, key=bucket_path + "versions/1.0.46.zip") print(res)