# MinIO 迁移到 boto3(S3 兼容协议) **日期**:2026-02-02 **类型**:架构升级 **影响范围**:后端 - 文件存储服务 --- ## 变更概述 将文件存储服务从 MinIO SDK 迁移到 boto3(S3 兼容协议),实现对多种对象存储的统一支持,包括阿里云 OSS、AWS S3、MinIO、腾讯云 COS 等。 --- ## 迁移原因 ### 1. **通用性需求** - 生产环境使用阿里云 OSS - 开发环境可能使用 MinIO - 未来可能需要支持 AWS S3 或其他云存储 ### 2. **S3 兼容协议优势** - boto3 是最成熟的对象存储 SDK - 阿里云 OSS 完全支持 S3 兼容 API - 一套代码适配多种存储,只需修改配置 ### 3. **灵活性** - 开发/测试/生产环境可以使用不同存储 - 便于迁移和多云部署 - 降低供应商锁定风险 --- ## 详细变更 ### 1. 依赖更新 **文件**:`server/requirements.txt` **移除**: ```txt minio==7.2.0 ``` **添加**: ```txt boto3==1.34.0 botocore==1.34.0 ``` --- ### 2. 配置更新 **文件**:`server/app/core/config.py` **变更前**: ```python class Settings(BaseSettings): # MinIO 配置 MINIO_ENDPOINT: str MINIO_ACCESS_KEY: str MINIO_SECRET_KEY: str MINIO_SECURE: bool = False MINIO_BUCKET_NAME: str = "jointo" MINIO_PUBLIC_URL: str STORAGE_PROVIDER: str = "minio" ``` **变更后**: ```python class Settings(BaseSettings): # S3 兼容存储配置 S3_ENDPOINT_URL: str S3_ACCESS_KEY_ID: str S3_SECRET_ACCESS_KEY: str S3_REGION: str = "cn-hangzhou" S3_BUCKET_NAME: str = "jointo" S3_PUBLIC_URL: str = "" # 可选,用于 CDN STORAGE_PROVIDER: str = "oss" # oss/s3/minio ``` --- ### 3. 环境变量更新 **文件**:`server/.env.example` **变更前**: ```bash MINIO_ENDPOINT=localhost:6185 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_SECURE=false MINIO_BUCKET_NAME=jointo MINIO_PUBLIC_URL=http://localhost:6185 STORAGE_PROVIDER=minio ``` **变更后**: ```bash # 生产环境(阿里云 OSS) S3_ENDPOINT_URL=https://oss-cn-hangzhou.aliyuncs.com S3_ACCESS_KEY_ID=your_access_key_id S3_SECRET_ACCESS_KEY=your_secret_access_key S3_REGION=cn-hangzhou S3_BUCKET_NAME=jointo S3_PUBLIC_URL=https://jointo.oss-cn-hangzhou.aliyuncs.com STORAGE_PROVIDER=oss # 开发环境(本地 MinIO) # S3_ENDPOINT_URL=http://localhost:9000 # S3_ACCESS_KEY_ID=minioadmin # S3_SECRET_ACCESS_KEY=minioadmin # S3_REGION=us-east-1 # S3_BUCKET_NAME=jointo-dev # S3_PUBLIC_URL=http://localhost:9000/jointo-dev # STORAGE_PROVIDER=minio ``` --- ### 4. StorageService 重写 **文件**:`server/app/core/storage.py` **核心变更**: #### 4.1 导入更新 ```python # 变更前 from minio import Minio from minio.error import S3Error # 变更后 import boto3 from botocore.exceptions import ClientError from botocore.client import Config ``` #### 4.2 客户端初始化 ```python # 变更前 self.client = Minio( settings.MINIO_ENDPOINT, access_key=settings.MINIO_ACCESS_KEY, secret_key=settings.MINIO_SECRET_KEY, secure=settings.MINIO_SECURE ) # 变更后 self.s3_client = boto3.client( 's3', endpoint_url=settings.S3_ENDPOINT_URL, aws_access_key_id=settings.S3_ACCESS_KEY_ID, aws_secret_access_key=settings.S3_SECRET_ACCESS_KEY, region_name=settings.S3_REGION, config=Config( signature_version='s3v4', s3={'addressing_style': 'virtual'} ) ) ``` #### 4.3 Bucket 检查 ```python # 变更前 if not self.client.bucket_exists(self.bucket_name): self.client.make_bucket(self.bucket_name) # 变更后 try: self.s3_client.head_bucket(Bucket=self.bucket_name) except ClientError as e: if e.response['Error']['Code'] == '404': self.s3_client.create_bucket(Bucket=self.bucket_name) ``` #### 4.4 文件上传 ```python # 变更前 self.client.put_object( self.bucket_name, object_name, BytesIO(data), len(data), content_type=content_type ) # 变更后 extra_args = {} if content_type: extra_args['ContentType'] = content_type self.s3_client.put_object( Bucket=self.bucket_name, Key=object_name, Body=data, **extra_args ) ``` #### 4.5 预签名 URL ```python # 变更前 return self.client.presigned_get_object( self.bucket_name, object_name, expires=timedelta(seconds=expires) ) # 变更后 url = self.s3_client.generate_presigned_url( 'get_object', Params={ 'Bucket': self.bucket_name, 'Key': object_name }, ExpiresIn=expires ) return url ``` #### 4.6 文件删除 ```python # 变更前 self.client.remove_object(self.bucket_name, object_name) # 变更后 self.s3_client.delete_object( Bucket=self.bucket_name, Key=object_name ) ``` #### 4.7 文件存在检查 ```python # 变更前 try: self.client.stat_object(self.bucket_name, object_name) return True except S3Error: return False # 变更后 try: self.s3_client.head_object( Bucket=self.bucket_name, Key=object_name ) return True except ClientError: return False ``` --- ### 5. Docker Compose 更新(可选) **文件**:`server/docker-compose.yml` 如果不再需要本地 MinIO 服务,可以移除: ```yaml # 移除 MinIO 服务 # minio: # image: minio/minio:latest # container_name: jointo-server-minio # ... ``` **注意**:如果开发环境仍需要本地对象存储,建议保留 MinIO 服务,只需修改环境变量配置即可。 --- ### 6. 文档更新 **文件**:`docs/requirements/backend/04-services/resource/file-storage-service.md` - ✅ 更新服务概述(S3 兼容协议) - ✅ 更新支持的存储提供商列表 - ✅ 更新 StorageService 实现代码 - ✅ 更新配置示例(不同环境) - ✅ 更新环境变量说明 - ✅ 更新文档版本(v2.1 → v3.0) --- ## 迁移步骤 ### 1. 更新依赖 ```bash # 在容器内执行 docker exec jointo-server-app pip uninstall minio -y docker exec jointo-server-app pip install boto3==1.34.0 ``` 或更新 `requirements.txt` 后重新构建镜像: ```bash docker-compose build app ``` ### 2. 更新环境变量 编辑 `server/.env` 文件,将 MinIO 配置替换为 S3 配置: ```bash # 阿里云 OSS 配置 S3_ENDPOINT_URL=https://oss-cn-hangzhou.aliyuncs.com S3_ACCESS_KEY_ID=your_oss_access_key_id S3_SECRET_ACCESS_KEY=your_oss_secret_access_key S3_REGION=cn-hangzhou S3_BUCKET_NAME=jointo-prod S3_PUBLIC_URL=https://jointo-prod.oss-cn-hangzhou.aliyuncs.com STORAGE_PROVIDER=oss ``` ### 3. 更新代码 - 更新 `app/core/config.py` - 更新 `app/core/storage.py` ### 4. 重启服务 ```bash docker-compose restart app ``` ### 5. 验证功能 ```bash # 测试文件上传 curl -X POST http://localhost:6170/api/v1/file-storage/upload \ -H "Authorization: Bearer $TOKEN" \ -F "file=@test.jpg" \ -F "category=test" ``` --- ## 兼容性说明 ### API 接口 - ✅ **无变更**:所有 API 接口保持不变 - ✅ **无影响**:前端代码无需修改 ### 数据库 - ✅ **无变更**:数据库表结构无需修改 - ✅ **兼容**:已存储的文件记录完全兼容 ### 业务逻辑 - ✅ **无变更**:FileStorageService 接口保持不变 - ✅ **透明**:其他服务(AttachmentService、ProjectResourceService 等)无需修改 --- ## 配置对照表 | MinIO 配置 | boto3 配置 | 说明 | |-----------|-----------|------| | `MINIO_ENDPOINT` | `S3_ENDPOINT_URL` | 需要添加协议前缀(http:// 或 https://) | | `MINIO_ACCESS_KEY` | `S3_ACCESS_KEY_ID` | 访问密钥 ID | | `MINIO_SECRET_KEY` | `S3_SECRET_ACCESS_KEY` | 访问密钥 Secret | | `MINIO_SECURE` | - | 通过 endpoint URL 的协议判断(https = secure) | | `MINIO_BUCKET_NAME` | `S3_BUCKET_NAME` | Bucket 名称 | | `MINIO_PUBLIC_URL` | `S3_PUBLIC_URL` | 公开访问 URL(可选) | | - | `S3_REGION` | 区域(新增,OSS 需要) | --- ## 不同存储提供商配置示例 ### 阿里云 OSS ```bash S3_ENDPOINT_URL=https://oss-cn-hangzhou.aliyuncs.com S3_ACCESS_KEY_ID=LTAI5t... S3_SECRET_ACCESS_KEY=xxx... S3_REGION=cn-hangzhou S3_BUCKET_NAME=jointo-prod S3_PUBLIC_URL=https://cdn.jointo.com # CDN 加速 STORAGE_PROVIDER=oss ``` ### AWS S3 ```bash S3_ENDPOINT_URL=https://s3.amazonaws.com S3_ACCESS_KEY_ID=AKIA... S3_SECRET_ACCESS_KEY=xxx... S3_REGION=us-east-1 S3_BUCKET_NAME=jointo-prod S3_PUBLIC_URL=https://jointo-prod.s3.amazonaws.com STORAGE_PROVIDER=s3 ``` ### MinIO(本地开发) ```bash S3_ENDPOINT_URL=http://localhost:9000 S3_ACCESS_KEY_ID=minioadmin S3_SECRET_ACCESS_KEY=minioadmin S3_REGION=us-east-1 S3_BUCKET_NAME=jointo-dev S3_PUBLIC_URL=http://localhost:9000/jointo-dev STORAGE_PROVIDER=minio ``` ### 腾讯云 COS ```bash S3_ENDPOINT_URL=https://cos.ap-guangzhou.myqcloud.com S3_ACCESS_KEY_ID=AKID... S3_SECRET_ACCESS_KEY=xxx... S3_REGION=ap-guangzhou S3_BUCKET_NAME=jointo-prod-1234567890 S3_PUBLIC_URL=https://jointo-prod-1234567890.cos.ap-guangzhou.myqcloud.com STORAGE_PROVIDER=cos ``` --- ## 测试建议 ### 1. 单元测试 ```python # tests/unit/test_storage_service.py import pytest from app.core.storage import StorageService @pytest.mark.asyncio async def test_upload_bytes(): storage = StorageService() data = b"test content" url = await storage.upload_bytes(data, "test/file.txt", "text/plain") assert url is not None assert "test/file.txt" in url ``` ### 2. 集成测试 ```bash # 上传测试 curl -X POST http://localhost:6170/api/v1/file-storage/upload \ -H "Authorization: Bearer $TOKEN" \ -F "file=@test.jpg" \ -F "category=test" # 获取预签名 URL curl -X GET "http://localhost:6170/api/v1/file-storage/presigned-url?storage_path=test/file.txt" \ -H "Authorization: Bearer $TOKEN" ``` ### 3. 性能测试 - 上传 1MB 文件:< 1s - 上传 10MB 文件:< 5s - 生成预签名 URL:< 100ms --- ## 回滚方案 如果迁移后出现问题,可以快速回滚: ### 1. 恢复依赖 ```bash docker exec jointo-server-app pip uninstall boto3 -y docker exec jointo-server-app pip install minio==7.2.0 ``` ### 2. 恢复配置 恢复 `.env` 文件中的 MinIO 配置 ### 3. 恢复代码 ```bash git checkout HEAD~1 server/app/core/storage.py git checkout HEAD~1 server/app/core/config.py ``` ### 4. 重启服务 ```bash docker-compose restart app ``` --- ## 注意事项 ### 1. **阿里云 OSS 特殊配置** - 使用虚拟主机风格(virtual hosted-style) - Endpoint 格式:`https://oss-.aliyuncs.com` - Bucket 域名:`https://.oss-.aliyuncs.com` ### 2. **签名版本** - boto3 默认使用 Signature Version 4 - 阿里云 OSS 完全支持 S3v4 签名 ### 3. **CDN 加速** - 生产环境建议配置 `S3_PUBLIC_URL` 为 CDN 域名 - 提升文件访问速度 ### 4. **权限配置** - 确保 AccessKey 有 Bucket 的读写权限 - 建议使用 RAM 子账号,遵循最小权限原则 --- ## 相关文档 - 需求文档:`docs/requirements/backend/04-services/resource/file-storage-service.md` - boto3 文档:https://boto3.amazonaws.com/v1/documentation/api/latest/index.html - 阿里云 OSS S3 兼容:https://help.aliyun.com/document_detail/64919.html - AWS S3 API:https://docs.aws.amazon.com/s3/ --- ## 作者 AI Assistant ## 审核状态 待审核