MinIO 提供兼容 Amazon S3 API 的对象存储能力。Python 应用通常负责上传文件、读取对象、生成临时下载地址和维护对象元数据,而不是把 Bucket 直接改成匿名公开。
本文以“TLS、私有 Bucket、最小权限凭据”为默认姿态。示例不会使用默认账号,也不会把 Access Key 和 Secret Key 写入源码。
安装 SDK
pip install -U minio部署环境提供以下变量:
export MINIO_ENDPOINT="storage.example.com:9000"export MINIO_ACCESS_KEY="<managed-access-key>"export MINIO_SECRET_KEY="<managed-secret-key>"export MINIO_SECURE="true"环境变量只用于演示配置入口。生产环境应由密钥管理服务注入,并确保进程日志、错误报告和诊断页面不会输出完整凭据。
初始化客户端
import os
from minio import Minio
def parse_bool(value: str) -> bool: normalized = value.strip().lower() if normalized in {"1", "true", "yes", "on"}: return True if normalized in {"0", "false", "no", "off"}: return False raise ValueError(f"invalid boolean value: {value!r}")
client = Minio( endpoint=os.environ["MINIO_ENDPOINT"], access_key=os.environ["MINIO_ACCESS_KEY"], secret_key=os.environ["MINIO_SECRET_KEY"], secure=parse_bool(os.getenv("MINIO_SECURE", "true")),)endpoint 只包含主机和端口,不应带 http:// 或 https://。协议由 secure 决定。
生产环境不要为了“先跑起来”而关闭证书校验。内部 CA 应通过系统信任链或受控 HTTP Client 配置提供,而不是长期使用明文 HTTP。
一个职责清晰的轻量封装
下面的封装只处理对象存储 API,不负责数据库事务、业务权限或文件内容安全检查。
from collections.abc import Iteratorfrom datetime import timedeltafrom pathlib import Path
from minio import Miniofrom minio.datatypes import Objectfrom minio.error import S3Error
class ObjectStorage: def __init__(self, client: Minio) -> None: self.client = client
def ensure_bucket(self, bucket_name: str) -> None: if not self.client.bucket_exists(bucket_name): self.client.make_bucket(bucket_name)
def upload_file( self, bucket_name: str, object_name: str, file_path: Path, *, content_type: str, ) -> None: self.client.fput_object( bucket_name=bucket_name, object_name=object_name, file_path=str(file_path), content_type=content_type, )
def download_file( self, bucket_name: str, object_name: str, destination: Path, ) -> None: self.client.fget_object( bucket_name=bucket_name, object_name=object_name, file_path=str(destination), )
def read_bytes( self, bucket_name: str, object_name: str, ) -> bytes: response = self.client.get_object(bucket_name, object_name) try: return response.read() finally: response.close() response.release_conn()
def list_objects( self, bucket_name: str, *, prefix: str = "", ) -> Iterator[Object]: return self.client.list_objects( bucket_name, prefix=prefix, recursive=True, )
def stat_object(self, bucket_name: str, object_name: str): return self.client.stat_object(bucket_name, object_name)
def delete_object(self, bucket_name: str, object_name: str) -> None: self.client.remove_object(bucket_name, object_name)
def presigned_download( self, bucket_name: str, object_name: str, *, valid_for: timedelta = timedelta(minutes=15), ) -> str: return self.client.presigned_get_object( bucket_name, object_name, expires=valid_for, )使用方式:
storage = ObjectStorage(client)storage.ensure_bucket("private-documents")
storage.upload_file( "private-documents", "reports/example.txt", Path("example.txt"), content_type="text/plain; charset=utf-8",)
url = storage.presigned_download( "private-documents", "reports/example.txt",)print(url)示例 Bucket 和对象名都是通用名称。真实系统中应使用不可猜测的业务对象 ID,并在数据库中保存对象与租户、权限和状态的关系。
为什么必须释放流式响应
get_object() 返回的是流式 HTTP Response。异常通常可能在真正读取数据时才出现。
正确结构是:
response = client.get_object(bucket_name, object_name)try: for chunk in response.stream(32 * 1024): consume(chunk)finally: response.close() response.release_conn()如果不调用 response.close() 和 response.release_conn(),底层连接可能不能及时回到连接池,持续流量下会逐渐耗尽可用连接。
不需要自己处理流时,优先使用 fget_object() 或 download_file() 等更高层方法。
Bucket 创建不是请求级操作
ensure_bucket() 适合部署初始化或明确的管理流程,不应在每次普通上传请求中都无条件创建 Bucket。
需要考虑:
- 创建 Bucket 的凭据权限通常高于上传对象;
- 多个实例同时创建会产生竞争;
- Region、对象锁定和版本控制需要在创建时确定;
- 业务请求不应该因为管理 API 短暂失败而创建未知状态。
更安全的做法是由基础设施或部署任务预先创建 Bucket,应用运行凭据只具备必要的对象读写权限。
私有 Bucket、公开策略和预签名 URL
私有 Bucket
默认选择。只有持有有效凭据且通过策略授权的服务才能访问对象。
适合:
- 用户上传文件;
- 报告和导出结果;
- 模型、数据集和中间产物;
- 内部备份;
- 受权限控制的图片与附件。
公开 Bucket Policy
公开策略会允许匿名访问某些对象。它适合真正公开、可永久缓存的静态资源,但发布前要确认对象中不包含:
- 用户标识;
- 私有文档;
- EXIF 或其他隐藏元数据;
- 内部路径和文件名;
- 后续不应公开的新对象。
不要为了让前端“能打开链接”就把整个 Bucket 设为公开。
预签名 URL
presigned_get_object() 为私有对象生成带过期时间的临时 URL。它适合浏览器或移动端直接下载对象,服务端无需代理完整文件流。
预签名 URL 本质上是临时凭证:
- 使用 HTTPS 传输;
- 有效期尽量短;
- 不写入公开日志和分析系统;
- 生成前重新校验用户权限;
- 对高敏感文件考虑一次性令牌或服务端代理;
- 不要认为删除页面上的 URL 就能撤销已经签发的链接。
上传安全
对象存储只负责保存字节,不会自动判断文件是否安全。
上传接口还应处理:
- 文件大小上限;
- Content-Type 白名单与实际内容检测;
- 文件名规范化;
- 对象路径不能由用户直接拼接;
- 恶意文件扫描;
- 压缩炸弹;
- 图片和文档的元数据清理;
- 服务端加密要求;
- 上传完成后的数据库状态更新。
不要只相信客户端提供的 Content-Type 和扩展名。
大文件与内存
read_bytes() 会把整个对象读入内存,只适合明确限制大小的小对象。
大文件应使用:
fget_object()直接写文件;- 分块流式处理;
- 浏览器直传的预签名 PUT;
- SDK 的分片上传能力;
- 明确的最大对象大小和超时。
Web API 代理大文件时,要避免同时在内存中保留上传请求体和对象存储响应。
对象名与覆盖
S3 兼容对象存储使用 Bucket + Object Name 定位对象。相同名称再次上传通常会覆盖当前对象或产生新版本,具体取决于版本控制配置。
推荐对象名:
<tenant-id>/<resource-type>/<stable-object-id>/<revision>不要直接使用未经处理的用户文件名作为完整对象路径。文件名可以作为受控元数据保存,真实对象名使用服务端生成的 ID。
错误处理
from minio.error import S3Error
def safe_stat( storage: ObjectStorage, bucket_name: str, object_name: str,): try: return storage.stat_object(bucket_name, object_name) except S3Error as error: if error.code == "NoSuchKey": return None raise不要对所有 S3Error 返回“文件不存在”。权限不足、证书错误、Bucket 不存在和服务不可用需要不同的告警与响应。
重试上传前要确认操作是否可能已经成功。使用稳定对象名覆盖写入可能是幂等的,但如果每次重试都生成新对象名,就会制造孤儿对象。
生产环境检查清单
1. Endpoint 和凭据是否由密钥管理服务注入2. 是否默认启用 TLS 并验证证书3. 应用账号是否只拥有必要 Bucket 和对象权限4. Bucket 是否由部署流程预创建5. 是否默认保持 Bucket 私有6. 生成预签名 URL 前是否重新校验权限7. 预签名 URL 是否使用较短有效期且避免进入日志8. get_object 的响应是否总在 finally 中 close 和 release_conn9. 上传是否限制大小并校验实际文件类型10. 对象名是否由服务端生成并隔离租户11. 大文件是否避免一次性读入内存12. 是否监控错误率、延迟、连接池、容量和孤儿对象官方参考
本文根据早期个人笔记重新整理,并结合当前官方文档进行了校对。