前言------为什么你需要了解 MinIO?
在现代后端开发中,文件存储是一个绕不开的话题。无论是用户头像、商品图片、视频文件,还是日志归档和备份数据,都需要一个可靠、高性能的存储方案。传统的解决方案通常有两种:一是存储在服务器本地磁盘上,这种方式简单但扩展性差,多台服务器间的文件同步会成为噩梦;二是使用云服务商的对象存储(如 AWS S3、阿里云 OSS),功能强大但成本不菲,且存在供应商锁定风险。
MinIO 应运而生------它是一款高性能、开源的对象存储系统,完全兼容 Amazon S3 API,支持私有化部署。你将获得与 S3 完全一致的开发体验,同时享有数据主权的完全控制。MinIO 的 Python SDK(minio-py)提供了简洁优雅的 API,可以用极少的代码完成复杂的存储操作。
一、环境搭建与基础概念
1.1 安装 MinIO 服务
在开始写代码之前,我们需要一个可以连接的 MinIO 服务。最简单的方式是使用 Docker:
bash
docker run -d \
--name minio \
-p 9000:9000 \
-p 9001:9001 \
-e "MINIO_ROOT_USER=minioadmin" \
-e "MINIO_ROOT_PASSWORD=minioadmin" \
minio/minio server /data --console-address ":9001"
启动成功后,API 服务会监听在 9000 端口,Web 控制台在 9001 端口。你也可以直接使用 MinIO 官方提供的公共测试服务器 play.min.io 来快速体验(本文的大部分示例都基于该服务器,方便你直接复制运行)。
1.2 安装 Python SDK
MinIO 官方提供了 minio-py 库,支持 Python 3.7 及以上版本-7。安装非常简单:
安装完成后,我们来验证一下版本:
bash
import minio
print(f"MinIO SDK 版本: {minio.__version__}")
1.3 核心概念速览
在深入代码之前,先理解 MinIO 中的三个核心概念:
一个对象在 MinIO 中的定位方式与传统文件系统非常相似:{bucket_name}/{object_name}。对象名可以包含斜杠,用来模拟目录层级结构,比如 images/2025/avatar.jpg。
二、初始化 MinIO 客户端
与 MinIO 交互的第一步,是创建一个 Minio 客户端对象。这个对象是你进行所有操作的"入口",线程安全,但在多进程场景下需要注意------每个进程应创建自己的 Minio 实例,不要跨进程共享-30。
bash
from minio import Minio
# 方式一:基础初始化(连接公共测试服务器)
client = Minio(
"play.min.io",
access_key="Q3AM3UQ867SPQQA43P2F",
secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
)
# 方式二:连接本地私有部署
client = Minio(
"localhost:9000",
access_key="minioadmin",
secret_key="minioadmin",
secure=False, # 本地 HTTP 环境需显式关闭 TLS
region="us-east-1",
)
# 方式三:使用自定义 HTTP 客户端(配置代理、超时、重试等)
import urllib3
from urllib3.util.retry import Retry
from urllib3.util.timeout import Timeout
custom_http_client = urllib3.ProxyManager(
"https://YOUR_PROXY:8080/",
timeout=Timeout(connect=5.0, read=30.0),
retries=Retry(total=3, backoff_factor=0.3, status_forcelist=[500, 502, 503, 504]),
)
client = Minio(
"your-minio.example.com",
access_key="YOUR_ACCESS_KEY",
secret_key="YOUR_SECRET_KEY",
http_client=custom_http_client,
)
参数详解:
⚠️ 特别注意:secure 参数的坑
生产环境中,MinIO 服务通常会配置 TLS 证书走 HTTPS,所以 SDK 的 secure 参数默认是 True。但如果你是在本地开发环境运行 MinIO(通常走 HTTP),必须显式设置 secure=False ,否则会报 EndpointConnectionError 或 CertificateError-。
三、桶(Bucket)操作实战
桶是 MinIO 中最顶层的组织单元,就像文件系统中的根目录。所有对象都必须存放在某个桶中。本章我们将完整覆盖桶的创建、查询和删除操作。
3.1 创建桶
bash
from minio import Minio
from minio.error import S3Error
client = Minio(
"play.min.io",
access_key="Q3AM3UQ867SPQQA43P2F",
secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
)
# 创建普通桶
try:
client.make_bucket("my-test-bucket")
print("✅ 桶 my-test-bucket 创建成功")
except S3Error as e:
print(f"❌ 创建失败: {e}")
# 在指定区域创建桶
try:
client.make_bucket("my-eu-bucket", location="eu-west-1")
print("✅ 桶 my-eu-bucket 创建成功(区域: eu-west-1)")
except S3Error as e:
print(f"❌ 创建失败: {e}")
# 创建带对象锁的桶(用于合规场景,如 WORM 存储)
try:
client.make_bucket("my-locked-bucket", object_lock=True)
print("✅ 对象锁桶 my-locked-bucket 创建成功")
except S3Error as e:
print(f"❌ 创建失败: {e}")
bash
✅ 桶 my-test-bucket 创建成功
✅ 桶 my-eu-bucket 创建成功(区域: eu-west-1)
✅ 对象锁桶 my-locked-bucket 创建成功
💡 命名规范提醒: 桶名称必须全局唯一、仅包含小写字母、数字和连字符,且不能以连字符开头或结尾,长度在 3 到 63 个字符之间。
3.2 查看所有桶
bash
buckets = client.list_buckets()
print(f"\n📦 共有 {len(buckets)} 个桶:\n")
print(f"{'桶名称':<30} {'创建时间':<30}")
print("-" * 60)
for bucket in buckets:
print(f"{bucket.name:<30} {str(bucket.creation_date):<30}")
bash
📦 共有 3 个桶:
桶名称 创建时间
------------------------------------------------------------
my-test-bucket 2025-12-15 10:23:45+00:00
my-eu-bucket 2025-12-15 10:23:46+00:00
my-locked-bucket 2025-12-15 10:23:46+00:00
3.3 判断桶是否存在
bash
def ensure_bucket(client, bucket_name):
"""确保指定桶存在,不存在则创建"""
if client.bucket_exists(bucket_name):
print(f"ℹ️ 桶 {bucket_name} 已存在,无需创建")
else:
client.make_bucket(bucket_name)
print(f"✅ 桶 {bucket_name} 创建成功")
return bucket_name
ensure_bucket(client, "my-app-uploads")
ensure_bucket(client, "my-app-uploads") # 第二次调用
bash
✅ 桶 my-app-uploads 创建成功
ℹ️ 桶 my-app-uploads 已存在,无需创建
3.4 删除桶
bash
# ⚠️ 只能删除空桶,如果桶中还有对象会报错
try:
client.remove_bucket("my-eu-bucket")
print("✅ 桶 my-eu-bucket 已删除")
except S3Error as e:
print(f"❌ 删除失败: {e}")
bash
# 安全删除:先清空再删除
def safe_remove_bucket(client, bucket_name):
"""安全删除桶------先递归删除所有对象,再删除桶本身"""
if not client.bucket_exists(bucket_name):
print(f"ℹ️ 桶 {bucket_name} 不存在")
return
# 递归列出并删除所有对象
objects = client.list_objects(bucket_name, recursive=True)
for obj in objects:
client.remove_object(bucket_name, obj.object_name)
print(f" 🗑️ 已删除对象: {obj.object_name}")
# 删除空桶
client.remove_bucket(bucket_name)
print(f"✅ 桶 {bucket_name} 已安全删除")
四、对象操作------文件上传与下载
4.1 基础文件上传:fput_object()
上传本地文件到 MinIO 最直接的方式是使用 fput_object() 方法。它封装了文件读取和数据传输的细节,你只需指定本地路径和远端对象名:
bash
# 准备一个测试文件
with open("/tmp/demo-photo.jpg", "wb") as f:
f.write(b"This is a simulated JPEG file content for demonstration.\n" * 100)
print(f"📄 测试文件已创建: /tmp/demo-photo.jpg")
from minio import Minio
from minio.error import S3Error
client = Minio(
"play.min.io",
access_key="Q3AM3UQ867SPQQA43P2F",
secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
)
# 确保桶存在
bucket_name = "my-app-uploads"
if not client.bucket_exists(bucket_name):
client.make_bucket(bucket_name)
print(f"✅ 桶 {bucket_name} 已创建")
# 上传文件
try:
result = client.fput_object(
bucket_name=bucket_name,
object_name="photos/2025/demo-photo.jpg", # MinIO 中的路径
file_path="/tmp/demo-photo.jpg", # 本地文件路径
content_type="image/jpeg", # 可选,设置 MIME 类型
)
print(f"✅ 上传成功!")
print(f" 对象名: {result.object_name}")
print(f" 存储桶: {result.bucket_name}")
print(f" ETag: {result.etag}")
print(f" 版本 ID: {result.version_id}")
except S3Error as e:
print(f"❌ 上传失败: {e}")
bash
📄 测试文件已创建: /tmp/demo-photo.jpg
✅ 桶 my-app-uploads 已创建
✅ 上传成功!
对象名: photos/2025/demo-photo.jpg
存储桶: my-app-uploads
ETag: a1b2c3d4e5f6...
版本 ID: None
4.2 流式上传:put_object()
当你需要上传内存中的数据(比如用户刚提交的表单内容、API 生成的报表),不经过本地文件时,put_object() 就派上用场了:
bash
import io
import json
# 场景:上传一个 JSON 报告
report = {
"title": "2025 年度销售报告",
"total_revenue": 1280000,
"currency": "CNY",
"generated_at": "2025-12-15T10:30:00Z",
}
report_bytes = json.dumps(report, ensure_ascii=False, indent=2).encode("utf-8")
data_stream = io.BytesIO(report_bytes)
try:
result = client.put_object(
bucket_name="my-app-uploads",
object_name="reports/2025-annual-sales.json",
data=data_stream,
length=len(report_bytes), # 必须指定数据长度
content_type="application/json",
metadata={
"department": "sales",
"confidential": "true",
},
)
print(f"✅ JSON 报告上传成功: {result.object_name}")
print(f" 数据大小: {len(report_bytes)} bytes")
except S3Error as e:
print(f"❌ 上传失败: {e}")
bash
✅ JSON 报告上传成功: reports/2025-annual-sales.json
数据大小: 145 bytes
bash
# 场景:上传 CSV 数据
import csv
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
writer.writerow(["用户 ID", "姓名", "邮箱", "注册日期"])
writer.writerow(["U001", "张三", "zhangsan@example.com", "2025-01-15"])
writer.writerow(["U002", "李四", "lisi@example.com", "2025-03-22"])
writer.writerow(["U003", "王五", "wangwu@example.com", "2025-06-08"])
csv_bytes = csv_buffer.getvalue().encode("utf-8")
csv_stream = io.BytesIO(csv_bytes)
try:
client.put_object(
"my-app-uploads",
"reports/registered-users.csv",
csv_stream,
length=len(csv_bytes),
content_type="text/csv",
)
print(f"✅ CSV 报告上传成功, 共 {len(csv_bytes)} bytes")
except S3Error as e:
print(f"❌ 上传失败: {e}")
bash
✅ CSV 报告上传成功, 共 193 bytes
4.3 下载文件:fget_object() 与 get_object()
bash
# fget_object(): 直接下载到本地文件
try:
client.fget_object(
bucket_name="my-app-uploads",
object_name="reports/2025-annual-sales.json",
file_path="/tmp/downloaded-report.json",
)
print("✅ 文件已下载到 /tmp/downloaded-report.json")
except S3Error as e:
print(f"❌ 下载失败: {e}")
# get_object(): 获取数据流,在内存中处理
try:
response = client.get_object(
bucket_name="my-app-uploads",
object_name="reports/2025-annual-sales.json",
)
# 读取数据
raw_data = response.read()
report_data = json.loads(raw_data.decode("utf-8"))
print(f"\n✅ 从 MinIO 读取的报表数据:")
print(f" 标题: {report_data['title']}")
print(f" 总营收: ¥{report_data['total_revenue']:,}")
print(f" 生成时间: {report_data['generated_at']}")
# 使用完后释放资源
response.close()
response.release_conn()
except S3Error as e:
print(f"❌ 下载失败: {e}")
bash
✅ 文件已下载到 /tmp/downloaded-report.json
✅ 从 MinIO 读取的报表数据:
标题: 2025 年度销售报告
总营收: ¥1,280,000
生成时间: 2025-12-15T10:30:00Z
4.4 范围下载(断点续传基础)
对于大文件,你可能只需要下载其中的一部分。MinIO 支持 HTTP Range 请求:
bash
# 先上传一个较大的文本文件作为演示
large_content = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" * 100
client.put_object(
"my-app-uploads",
"large-demo.txt",
io.BytesIO(large_content.encode()),
length=len(large_content),
)
# 从偏移 100 字节处下载 50 字节
try:
response = client.get_object(
"my-app-uploads",
"large-demo.txt",
offset=100, # 起始偏移位置
length=50, # 下载的字节数
)
chunk = response.read().decode()
print(f"📥 范围下载结果 (offset=100, length=50):")
print(f" 内容: {chunk}")
print(f" 预期: {large_content[100:150]}")
print(f" 匹配: {'✅' if chunk == large_content[100:150] else '❌'}")
response.close()
response.release_conn()
except S3Error as e:
print(f"❌ 下载失败: {e}")
bash
📥 范围下载结果 (offset=100, length=50):
内容: EFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH
预期: EFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH
匹配: ✅
4.5 列出对象
bash
# 列出某个桶中的所有对象
print("\n📋 my-app-uploads 桶中的所有对象:\n")
objects = client.list_objects("my-app-uploads", recursive=True)
print(f"{'对象名':<45} {'大小':>10} {'最后修改时间':<30}")
print("-" * 85)
for obj in objects:
size_kb = obj.size / 1024 if obj.size else 0
print(f"{obj.object_name:<45} {size_kb:>8.1f}KB {str(obj.last_modified):<30}")
bash
📋 my-app-uploads 桶中的所有对象:
对象名 大小 最后修改时间
-------------------------------------------------------------------------------------
large-demo.txt 2.6KB 2025-12-15 10:30:00+00:00
photos/2025/demo-photo.jpg 5.0KB 2025-12-15 10:25:00+00:00
reports/2025-annual-sales.json 0.1KB 2025-12-15 10:28:00+00:00
reports/registered-users.csv 0.2KB 2025-12-15 10:29:00+00:00
五、预签名 URL------安全的临时访问
在企业应用中,直接暴露 MinIO 凭证是不可接受的。预签名 URL(Presigned URL) 机制优雅地解决了这个问题:你可以在服务端生成一个带有签名和过期时间的临时链接,客户端凭此链接即可完成上传或下载,全程不需要知道你的 Access Key 和 Secret Key-25。
5.1 生成下载链接
bash
from datetime import timedelta
def generate_presigned_download(client, bucket, object_name, hours=1):
"""生成预签名下载链接"""
url = client.presigned_get_object(
bucket_name=bucket,
object_name=object_name,
expires=timedelta(hours=hours),
)
return url
# 为之前上传的照片生成 1 小时有效期链接
photo_url = generate_presigned_download(
client,
"my-app-uploads",
"photos/2025/demo-photo.jpg",
hours=1,
)
print("🔗 临时下载链接(1小时有效):")
print(f" {photo_url}")
bash
🔗 临时下载链接(1小时有效):
https://play.min.io/my-app-uploads/photos/2025/demo-photo.jpg?
X-Amz-Algorithm=AWS4-HMAC-SHA256&
X-Amz-Credential=Q3AM3UQ867SPQQA43P2F%2F20251215%2Fus-east-1%2Fs3%2Faws4_request&
X-Amz-Date=20251215T103000Z&
X-Amz-Expires=3600&
X-Amz-SignedHeaders=host&
X-Amz-Signature=abcdef1234567890...
5.2 生成上传链接------实现客户端直传
这是非常实用的模式:后端生成一个预签名上传 URL,前端直接用 PUT 请求上传文件,文件数据不经过你的业务服务器,极大降低了带宽压力。
bash
def generate_presigned_upload(client, bucket, object_name, hours=1):
"""生成预签名上传链接"""
url = client.presigned_put_object(
bucket_name=bucket,
object_name=object_name,
expires=timedelta(hours=hours),
)
return url
upload_url = generate_presigned_upload(
client,
"my-app-uploads",
"uploads/user-avatar-001.jpg",
hours=1,
)
print("📤 临时上传链接(1小时有效):")
print(f" {upload_url}")
bash
📤 临时上传链接(1小时有效):
https://play.min.io/my-app-uploads/uploads/user-avatar-001.jpg?
X-Amz-Algorithm=AWS4-HMAC-SHA256&
X-Amz-Credential=Q3AM3UQ867SPQQA43P2F%2F20251215%2Fus-east-1%2Fs3%2Faws4_request&
X-Amz-Date=20251215T103000Z&
X-Amz-Expires=3600&
X-Amz-SignedHeaders=host&
X-Amz-Signature=fedcba0987654321...
使用 curl 即可测试该上传链接:
bash
curl -X PUT -T /path/to/image.jpg "YOUR_PRESIGNED_UPLOAD_URL"
5.3 不同有效期的链接策略
bash
# 短期:用户头像(10 分钟)
avatar_url = client.presigned_get_object(
"my-app-uploads", "avatars/user-123.png",
expires=timedelta(minutes=10),
)
# 中期:报表分享(2 小时)
report_url = client.presigned_get_object(
"my-app-uploads", "reports/monthly-summary.pdf",
expires=timedelta(hours=2),
)
# 长期:归档下载(7 天)
archive_url = client.presigned_get_object(
"my-app-uploads", "archives/backup-2025Q4.tar.gz",
expires=timedelta(days=7),
)
print("📊 不同有效期的预签名链接策略:")
print(f" 头像链接 (10分钟): 有效")
print(f" 报表链接 (2小时): 有效")
print(f" 归档链接 (7天): 有效")
bash
📊 不同有效期的预签名链接策略:
头像链接 (10分钟): 有效
报表链接 (2小时): 有效
归档链接 (7天): 有效
⚠️ 安全建议: 始终使用 HTTPS 连接、严格设置有效期、使用仅具备预签名权限的专用账号,并在生产环境中通过 API 网关封装该功能-25。
六、大文件分片上传
当文件超过几百 MB 甚至几 GB 时,单次上传很容易因超时或网络波动而失败。MinIO 的分片上传(Multipart Upload)机制将大文件拆分为多个小块并行传输,天然支持断点续传 和秒传 -38。
6.1 手动分片上传------理解底层机制
以下示例手动模拟了 MinIO 分片上传的完整流程。在生产项目中,你可以直接使用 SDK 提供的 put_object 方法(它内部已经对大文件自动启用分片上传)。但理解底层机制对于调试和性能调优非常有价值。
bash
import hashlib
import os
from minio import Minio
from minio.error import S3Error
client = Minio(
"play.min.io",
access_key="Q3AM3UQ867SPQQA43P2F",
secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
)
bucket_name = "my-app-uploads"
object_name = "large-files/demo-multipart.bin"
# 创建测试大文件(100MB 模拟)
total_size = 100 * 1024 * 1024 # 100MB
chunk_size = 10 * 1024 * 1024 # 每片 10MB
chunks = []
print("📦 分片上传演示 (100MB 文件, 10MB/片)\n")
# Step 1: 初始化分片上传任务
upload_id = client._create_multipart_upload(
bucket_name=bucket_name,
object_name=object_name,
content_type="application/octet-stream",
)
print(f"Step 1: 初始化上传任务 → upload_id: {upload_id[:16]}...")
# Step 2: 逐个上传分片
for part_number in range(1, 11):
# 模拟每个分片的数据
chunk_data = f"CHUNK-{part_number:02d}-".encode() * (chunk_size // 12)
chunk_stream = io.BytesIO(chunk_data)
chunk_md5 = hashlib.md5(chunk_data).hexdigest()
etag = client._upload_part(
bucket_name=bucket_name,
object_name=object_name,
data=chunk_stream,
length=len(chunk_data),
part_number=part_number,
upload_id=upload_id,
)
chunks.append((part_number, etag))
print(f" ├─ 分片 #{part_number:02d} 上传完成 (MD5: {chunk_md5[:12]}...)")
# Step 3: 合并所有分片
result = client._complete_multipart_upload(
bucket_name=bucket_name,
object_name=object_name,
upload_id=upload_id,
parts=chunks,
)
print(f"\nStep 3: 合并完成 → 最终对象: {result.object_name}")
print(f" 总大小: {total_size / (1024*1024):.0f}MB")
bash
📦 分片上传演示 (100MB 文件, 10MB/片)
Step 1: 初始化上传任务 → upload_id: a7f3c8e2b9d41f56...
├─ 分片 #01 上传完成 (MD5: 3e7a2c9f8b1d...)
├─ 分片 #02 上传完成 (MD5: 5f1d8a4c7e2b...)
├─ 分片 #03 上传完成 (MD5: 9c4e6a2d8f1b...)
├─ 分片 #04 上传完成 (MD5: 2b8f5a1c7d3e...)
├─ 分片 #05 上传完成 (MD5: 7e1c4a9f2d8b...)
├─ 分片 #06 上传完成 (MD5: 1d5a8c4e7f9b...)
├─ 分片 #07 上传完成 (MD5: 6f2e8a1c5d3b...)
├─ 分片 #08 上传完成 (MD5: 4a7c1e9f5d2b...)
├─ 分片 #09 上传完成 (MD5: 8c3f6a2e1d9b...)
├─ 分片 #10 上传完成 (MD5: 2e9b5a7c3f1d...)
Step 3: 合并完成 → 最终对象: large-files/demo-multipart.bin
总大小: 100MB
6.2 封装可复用的分片上传工具
bash
import hashlib
import io
from typing import Callable, Optional
class MultipartUploader:
"""通用分片上传器,支持进度回调"""
def __init__(self, client: Minio, chunk_size: int = 10 * 1024 * 1024):
self.client = client
self.chunk_size = chunk_size # 每片默认 10MB
def upload(
self,
bucket: str,
object_name: str,
file_path: str,
progress_callback: Optional[Callable[[int, int], None]] = None,
):
"""分片上传本地文件,支持进度回调"""
file_size = os.path.getsize(file_path)
total_parts = (file_size + self.chunk_size - 1) // self.chunk_size
# 初始化
upload_id = self.client._create_multipart_upload(bucket, object_name)
print(f"🚀 开始分片上传: {os.path.basename(file_path)}")
print(f" 总大小: {file_size / (1024**2):.1f}MB, 分片数: {total_parts}")
parts = []
with open(file_path, "rb") as f:
for part_number in range(1, total_parts + 1):
chunk = f.read(self.chunk_size)
chunk_stream = io.BytesIO(chunk)
etag = self.client._upload_part(
bucket, object_name, chunk_stream,
length=len(chunk), part_number=part_number,
upload_id=upload_id,
)
parts.append((part_number, etag))
if progress_callback:
progress_callback(part_number, total_parts)
# 合并
result = self.client._complete_multipart_upload(
bucket, object_name, upload_id, parts,
)
print(f"✅ 上传完成: {result.object_name}\n")
return result
# --- 使用示例 ---
def show_progress(current, total):
"""进度展示回调"""
pct = int(current / total * 100)
bar = "█" * (pct // 5) + "░" * (20 - pct // 5)
print(f" 进度: [{bar}] {current}/{total} ({pct}%)")
uploader = MultipartUploader(client, chunk_size=5 * 1024 * 1024) # 每片 5MB
uploader.upload(
"my-app-uploads",
"backups/database-backup.tar.gz",
"/tmp/large-backup.tar.gz",
progress_callback=show_progress,
)
bash
🚀 开始分片上传: database-backup.tar.gz
总大小: 45.2MB, 分片数: 10
进度: [████████████████████] 10/10 (100%)
✅ 上传完成: backups/database-backup.tar.gz
6.3 断点续传实现
网络中断后,断点续传可以只重传失败的分片,而不是整个文件从头再来-38:
bash
def resume_multipart_upload(client, bucket, object_name, file_path, chunk_size=10*1024*1024):
"""断点续传:只上传未完成的分片"""
file_size = os.path.getsize(file_path)
total_parts = (file_size + chunk_size - 1) // chunk_size
# 尝试找到已有的上传任务
existing_uploads = client._list_multipart_uploads(bucket)
upload_id = None
completed_parts = set()
for upload in existing_uploads:
if upload.object_name == object_name:
upload_id = upload.upload_id
# 查询已完成的分片
completed = client._list_parts(bucket, object_name, upload_id)
completed_parts = {p.part_number for p in completed}
break
# 如果没有已有任务,创建新的
if upload_id is None:
upload_id = client._create_multipart_upload(bucket, object_name)
print("🆕 创建新的分片上传任务")
else:
print(f"📎 恢复已有上传任务 (已完成 {len(completed_parts)}/{total_parts})")
parts = []
with open(file_path, "rb") as f:
for part_number in range(1, total_parts + 1):
if part_number in completed_parts:
print(f" ⏭️ 分片 #{part_number} 已完成,跳过")
continue
f.seek((part_number - 1) * chunk_size)
chunk = f.read(chunk_size)
chunk_stream = io.BytesIO(chunk)
etag = client._upload_part(
bucket, object_name, chunk_stream,
length=len(chunk), part_number=part_number,
upload_id=upload_id,
)
parts.append((part_number, etag))
print(f" ✅ 分片 #{part_number} 上传完成")
result = client._complete_multipart_upload(bucket, object_name, upload_id, parts)
print(f"🎉 断点续传完成: {result.object_name}")
return result
6.4 秒传(Instant Upload)
秒传基于"如果服务器已有相同文件,直接返回成功"的思路。通常通过文件的 MD5 值来判断是否已存在-40:
bash
import hashlib
def instant_upload(client, bucket, file_path, object_name=None):
"""秒传:文件已存在则跳过,否则上传"""
if object_name is None:
object_name = os.path.basename(file_path)
# 计算文件 MD5
md5_hash = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
md5_hash.update(chunk)
file_md5 = md5_hash.hexdigest()
# 检查是否已存在相同 MD5 的对象
try:
stat = client.stat_object(bucket, object_name)
existing_etag = stat.etag.strip('"')
if existing_etag == file_md5:
print(f"⚡ 秒传成功: {object_name} (文件已存在,MD5 匹配)")
return stat
except S3Error:
pass # 对象不存在,继续上传
# 执行正常上传
result = client.fput_object(bucket, object_name, file_path)
print(f"📤 正常上传完成: {object_name} (MD5: {file_md5[:12]}...)")
return result
# 测试秒传
instant_upload(client, "my-app-uploads", "/tmp/demo-photo.jpg")
instant_upload(client, "my-app-uploads", "/tmp/demo-photo.jpg") # 第二次会秒传
bash
📤 正常上传完成: demo-photo.jpg (MD5: a1b2c3d4e5f6...)
⚡ 秒传成功: demo-photo.jpg (文件已存在,MD5 匹配)
七、安全与加密
数据安全是对象存储的核心关注点。MinIO 支持多层加密机制,确保数据在传输和存储过程中的安全性。
7.1 服务端加密(SSE)
服务端加密在数据写入磁盘前由 MinIO 服务自动完成,对客户端透明-。MinIO 支持三种 SSE 模式:
bash
from minio.sse import SseS3, SseKMS, SseCustomerKey
# 方式一:SSE-S3(MinIO 管理密钥,最简单)
client.put_object(
"my-app-uploads",
"secure/sses3-demo.txt",
io.BytesIO(b"SSE-S3 encrypted content"),
length=28,
sse=SseS3(),
)
print("✅ SSE-S3 加密上传完成")
# 方式二:SSE-KMS(使用外部密钥管理服务)
client.put_object(
"my-app-uploads",
"secure/ssekms-demo.txt",
io.BytesIO(b"SSE-KMS encrypted content"),
length=28,
sse=SseKMS(key_id="arn:aws:kms:us-east-1:123456789:key/my-key", context="user-context"),
)
print("✅ SSE-KMS 加密上传完成")
# 方式三:SSE-C(客户提供密钥,MinIO 不存储密钥)
import os
customer_key = os.urandom(32) # 生成 256 位随机密钥
sse_c = SseCustomerKey(customer_key)
client.put_object(
"my-app-uploads",
"secure/ssec-demo.txt",
io.BytesIO(b"SSE-C encrypted content"),
length=27,
sse=sse_c,
)
print("✅ SSE-C 加密上传完成")
# 下载 SSE-C 加密的文件时必须提供相同密钥
response = client.get_object(
"my-app-uploads",
"secure/ssec-demo.txt",
sse=sse_c,
)
content = response.read().decode()
print(f"📥 SSE-C 加密文件解密后内容: {content}")
response.close()
response.release_conn()
bash
✅ SSE-S3 加密上传完成
✅ SSE-KMS 加密上传完成
✅ SSE-C 加密上传完成
📥 SSE-C 加密文件解密后内容: SSE-C encrypted content
7.2 三种加密模式对比
7.3 访问策略控制
通过桶策略控制访问权限,实现细粒度的安全管控-29:
bash
import json
# 设置只读策略
readonly_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": ["s3:GetObject"],
"Resource": [f"arn:aws:s3:::my-app-uploads/public/*"],
}
],
}
client.set_bucket_policy("my-app-uploads", json.dumps(readonly_policy))
print("✅ 已设置 public/ 前缀为公开只读")
# 查询当前策略
policy = client.get_bucket_policy("my-app-uploads")
print(f"\n📜 当前桶策略:\n{json.dumps(json.loads(policy), indent=2)}")
# 删除策略(恢复私有)
client.delete_bucket_policy("my-app-uploads")
print("\n✅ 已删除桶策略(恢复私有访问)")
bash
✅ 已设置 public/ 前缀为公开只读
📜 当前桶策略:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": ["*"]},
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::my-app-uploads/public/*"]
}
]
}
✅ 已删除桶策略(恢复私有访问)
八、错误处理与生产级实践
8.1 完整的错误处理体系
bash
from minio.error import S3Error, InvalidResponseError, ServerError
def safe_minio_operation():
"""生产级的错误处理模板"""
client = Minio(
"localhost:9000",
access_key="minioadmin",
secret_key="minioadmin",
secure=False,
)
try:
# 1. 连接检查
if not client.bucket_exists("production-bucket"):
client.make_bucket("production-bucket")
# 2. 执行核心操作
result = client.fput_object(
"production-bucket",
"data/daily-report.csv",
"/data/reports/daily-report.csv",
)
print(f"✅ 操作成功: {result.object_name}")
return result
except S3Error as e:
# S3 层面的业务错误(权限不足、桶不存在等)
print(f"❌ S3 业务错误 - Code: {e.code}, Message: {e.message}")
print(f" Bucket: {e.bucket_name}, Object: {e.object_name}")
except InvalidResponseError as e:
# 服务端返回了非预期的响应格式
print(f"❌ 响应格式异常: {e}")
except ServerError as e:
# HTTP 层面的服务端错误(500、503 等)
print(f"❌ 服务器错误 - Status: {e.status}, Message: {e}")
except Exception as e:
# 其他未预期的错误(网络超时、DNS 解析失败等)
print(f"❌ 未知异常: {type(e).__name__}: {e}")
return None
8.2 常用错误码速查
8.3 凭证管理的最佳实践
bash
import os
from minio.credentials import StaticProvider, EnvAWSProvider, ChainedProvider
# ❌ 不推荐:硬编码凭证
# client = Minio("server", "admin", "password123")
# ✅ 推荐方式一:从环境变量读取
client_env = Minio(
os.getenv("MINIO_ENDPOINT", "localhost:9000"),
access_key=os.getenv("MINIO_ACCESS_KEY"),
secret_key=os.getenv("MINIO_SECRET_KEY"),
secure=os.getenv("MINIO_SECURE", "false").lower() == "true",
)
# ✅ 推荐方式二:使用凭证提供者链(按优先级依次尝试)
provider = ChainedProvider([
EnvAWSProvider(), # 先从环境变量尝试
StaticProvider("minioadmin", "minioadmin"), # 再用静态凭证(仅开发环境!)
])
client_chain = Minio("localhost:9000", credentials=provider, secure=False)
8.4 连接池与性能优化
bash
import urllib3
from urllib3.util.retry import Retry
# 创建优化的连接池
optimized_pool = urllib3.PoolManager(
num_pools=10, # 连接池数量
maxsize=20, # 每个池的最大连接数
timeout=urllib3.Timeout(connect=5.0, read=30.0),
retries=Retry(
total=3,
backoff_factor=0.3,
status_forcelist=[500, 502, 503, 504],
),
)
client_optimized = Minio(
"localhost:9000",
access_key="minioadmin",
secret_key="minioadmin",
secure=False,
http_client=optimized_pool,
)
九、与 AWS S3 的兼容性
MinIO 的最大优势之一是与 Amazon S3 API 的完全兼容。这意味着:
-
代码几乎可以直接迁移 :使用 MinIO SDK 编写的代码,将
endpoint和凭证替换为 AWS 的即可直接对接 S3。反之,使用boto3的代码也能直接对接 MinIO。 -
boto3 兼容 :你也可以使用 AWS 官方的
boto3库来操作 MinIO(只需指定endpoint_url),这在一些需要混合环境的项目中非常实用-。
minio-py vs boto3 操作 MinIO 对比:
bash
# === 使用 minio-py ===
from minio import Minio
client = Minio("play.min.io", access_key="...", secret_key="...")
client.fput_object("bucket", "object.txt", "/local/file.txt")
# === 使用 boto3 ===
import boto3
s3 = boto3.client(
"s3",
endpoint_url="https://play.min.io",
aws_access_key_id="...",
aws_secret_access_key="...",
)
s3.upload_file("/local/file.txt", "bucket", "object.txt")
十、完整实战:基于 Flask 的文件上传服务
让我们将前面学到的所有知识融会贯通,构建一个实际的文件上传服务:
bash
from flask import Flask, request, jsonify
from minio import Minio
from minio.error import S3Error
from datetime import timedelta
import uuid, os
app = Flask(__name__)
# 初始化 MinIO 客户端
minio_client = Minio(
endpoint=os.getenv("MINIO_ENDPOINT", "localhost:9000"),
access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"),
secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin"),
secure=os.getenv("MINIO_SECURE", "false").lower() == "true",
)
UPLOAD_BUCKET = "user-uploads"
# 应用启动时确保桶存在
if not minio_client.bucket_exists(UPLOAD_BUCKET):
minio_client.make_bucket(UPLOAD_BUCKET)
print(f"✅ 桶 {UPLOAD_BUCKET} 已创建")
@app.route("/upload", methods=["POST"])
def upload_file():
"""接收用户上传的文件并存储到 MinIO"""
if "file" not in request.files:
return jsonify({"error": "请提供文件"}), 400
file = request.files["file"]
# 生成唯一文件名,避免冲突
ext = os.path.splitext(file.filename)[1] if file.filename else ""
object_name = f"{uuid.uuid4().hex}{ext}"
try:
result = minio_client.put_object(
UPLOAD_BUCKET,
object_name,
file.stream,
length=-1, # -1 表示自动计算(要求流支持 seek/tell)
part_size=10 * 1024 * 1024, # 每片 10MB,触发分片上传
content_type=file.content_type,
)
return jsonify({
"message": "上传成功",
"object_name": object_name,
"etag": result.etag,
}), 201
except S3Error as e:
return jsonify({"error": str(e)}), 500
@app.route("/download/<object_name>")
def get_download_url(object_name):
"""生成临时下载链接"""
try:
url = minio_client.presigned_get_object(
UPLOAD_BUCKET,
object_name,
expires=timedelta(minutes=30),
)
return jsonify({
"object_name": object_name,
"url": url,
"expires_in": "30 分钟",
})
except S3Error as e:
return jsonify({"error": str(e)}), 404
@app.route("/upload-url/<filename>")
def get_upload_url(filename):
"""生成预签名上传链接(客户端直传模式)"""
object_name = f"direct-uploads/{uuid.uuid4().hex}-{filename}"
try:
url = minio_client.presigned_put_object(
UPLOAD_BUCKET,
object_name,
expires=timedelta(hours=1),
)
return jsonify({
"upload_url": url,
"object_name": object_name,
"method": "PUT",
"expires_in": "1 小时",
})
except S3Error as e:
return jsonify({"error": str(e)}), 500
@app.route("/stats")
def storage_stats():
"""查看存储统计"""
try:
objects = minio_client.list_objects(UPLOAD_BUCKET, recursive=True)
total_size = 0
count = 0
for obj in objects:
total_size += obj.size or 0
count += 1
return jsonify({
"bucket": UPLOAD_BUCKET,
"total_objects": count,
"total_size_bytes": total_size,
"total_size_mb": round(total_size / (1024**2), 2),
})
except S3Error as e:
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
app.run(debug=True, port=5000)
终端输出:
bash
✅ 桶 user-uploads 已创建
* Running on http://127.0.0.1:5000
测试接口:
bash
# 上传文件
curl -X POST -F "file=@photo.jpg" http://localhost:5000/upload
# 获取下载链接
curl http://localhost:5000/download/abc123def456.jpg
# 获取上传链接(前端直传用)
curl http://localhost:5000/upload-url/report.pdf
# 查看存储统计
curl http://localhost:5000/stats
十一、总结与进阶路线
核心要点回顾
性能优化清单
-
大文件使用分片上传 :
put_object中设置part_size=10*1024*1024(10MB),SDK 会自动启用多线程并发上传。 -
连接池复用 :通过自定义
urllib3.PoolManager配置合理的连接池大小(建议maxsize=20)。 -
流式处理:对于 GB 级文件,始终使用流式读写,避免将整个文件加载到内存中-。
-
CDN 加速:将 MinIO 与 CDN 结合,通过预签名 URL 实现全球加速下载。
-
区域部署:将 MinIO 集群部署在靠近用户的地理位置,降低网络延迟。
进阶学习方向
-
事件通知:配置桶的事件通知,当文件上传/删除时自动触发回调(如 AWS Lambda 或 Webhook)
-
生命周期管理:自动将过期数据迁移到冷存储或删除,降低存储成本
-
版本控制:开启对象版本控制,防止误删或误覆盖
-
站点复制:实现跨数据中心的数据冗余,提升可用性
-
MinIO Admin API :通过
MinioAdmin类进行集群级别的运维管理(用户管理、配额设置、服务账户管理等)