438 lines
12 KiB
Markdown
438 lines
12 KiB
Markdown
# S3素材统一迁移方案
|
||
|
||
## 背景
|
||
|
||
当前系统中素材分散在两个S3存储位置:
|
||
- **老位置**:`s3.1080p.hk/jsxiangche` - 老素材
|
||
- **新位置**:`s3.yydd.ai/gongxiangpan` - 新素材
|
||
|
||
**目标**:将所有素材统一迁移到新位置 `s3.yydd.ai/gongxiangpan`,方便后期维护。
|
||
|
||
---
|
||
|
||
## 项目存储结构
|
||
|
||
### 文件存储方式
|
||
1. **源文件(压缩包)** → 本地 `./uploads` 目录(Docker挂载)- **不需要迁移**(1,411个,699.5 GB)
|
||
2. **音频/视频源文件** → MinIO S3(zipFileName字段)- **需要迁移**(97个在老S3)
|
||
3. **预览图/视频转码文件** → MinIO S3 - **需要迁移**
|
||
|
||
### 数据库中的S3相关字段
|
||
| 字段 | 说明 | 存储内容 |
|
||
|------|------|----------|
|
||
| previewUrl | 预览图URL | 图片(webp/jpg) |
|
||
| zipFileName | 源文件名/音频视频URL | 本地文件名 或 S3音频/视频URL |
|
||
| zipFilePath | 源文件路径 | 本地路径(uploads/xxx 或 /app/uploads/xxx) |
|
||
| displayImages | 详情图数组 | 多张图片URL |
|
||
| videoSourceUrl | 视频源文件 | S3视频URL |
|
||
| videoPreview540Url | 540P预览视频 | S3视频URL |
|
||
| videoPreviewClipUrl | 视频片段 | S3视频URL |
|
||
| videoCoverUrl | 视频封面 | S3图片URL |
|
||
|
||
**重要说明**:
|
||
- `zipFilePath` 存储本地路径,有两种格式:`uploads/xxx.zip` 或 `/app/uploads/xxx.zip`
|
||
- 音频/视频类素材的源文件存储在 `zipFileName` 字段(S3 URL),`zipFilePath` 为空
|
||
|
||
---
|
||
|
||
## 迁移数据量统计
|
||
|
||
### 需要迁移的S3文件(从 s3.1080p.hk 到 s3.yydd.ai)
|
||
|
||
| 字段 | 总数 | 老S3 (需迁移) | 新S3 (已在目标) | 空/本地 |
|
||
|------|------|---------------|-----------------|---------|
|
||
| previewUrl | 1,525 | **1,114** | 313 | 97 |
|
||
| zipFileName (音频) | 1,525 | **97** | 13 | 1,415 |
|
||
| videoSourceUrl | 1,525 | **4** | 13 | 1,401 |
|
||
| videoPreview540Url | 1,525 | **122** | 1 | 1,402 |
|
||
| videoPreviewClipUrl | 1,525 | **122** | 1 | 1,402 |
|
||
| videoCoverUrl | 1,525 | **78** | 45 | 1,402 |
|
||
| displayImages | 2,665张 | **2,358** | 307 | - |
|
||
|
||
### 需要迁移的文件总数
|
||
- previewUrl: 1,114个
|
||
- zipFileName (音频): 97个
|
||
- videoSourceUrl: 4个
|
||
- videoPreview540Url: 122个
|
||
- videoPreviewClipUrl: 122个
|
||
- videoCoverUrl: 78个
|
||
- displayImages: 2,358个
|
||
- **总计:约 3,895 个文件**
|
||
|
||
---
|
||
|
||
## 文件大小估算
|
||
|
||
### 1. 音频/视频源文件(zipFileName字段,已知精确大小)
|
||
从数据库 `zipFileSize` 字段获取:
|
||
- 音乐:93个,**523.11 MB**(平均 5.62 MB/个)
|
||
- 主图视频源文件:4个,**183.06 MB**(平均 45.76 MB/个)
|
||
- **音频/视频源文件总计:706.17 MB**
|
||
|
||
### 2. 预览图(估算)
|
||
- 数量:1,114个
|
||
- 平均大小:约 50KB
|
||
- **预览图总计:约 55.7 MB**
|
||
|
||
### 3. 详情图(估算)
|
||
- 数量:2,358个
|
||
- 平均大小:约 100KB
|
||
- **详情图总计:约 235.8 MB**
|
||
|
||
### 4. 视频转码文件(估算)
|
||
视频时长统计:
|
||
- 种草视频:56个,总时长 16.53分钟
|
||
- 主图视频:39个,总时长 17.18分钟
|
||
- 安装视频:27个,总时长 77.33分钟
|
||
- **总时长:约 111分钟**
|
||
|
||
估算大小(540P视频约 2MB/分钟,片段约 0.5MB/分钟):
|
||
- videoPreview540Url:122个,约 **222 MB**
|
||
- videoPreviewClipUrl:122个,约 **55 MB**
|
||
- videoCoverUrl:78个,约 **4 MB**
|
||
- **视频转码文件总计:约 281 MB**
|
||
|
||
### 总数据量
|
||
| 类型 | 文件数 | 大小 |
|
||
|------|--------|------|
|
||
| 音频/视频源文件 | 101 | 706 MB |
|
||
| 预览图 | 1,114 | 56 MB |
|
||
| 详情图 | 2,358 | 236 MB |
|
||
| 视频转码文件 | 322 | 281 MB |
|
||
| **总计** | **3,895** | **约 1.28 GB**
|
||
|
||
---
|
||
|
||
## 迁移时间估算
|
||
|
||
### 网络速度参考
|
||
根据网络诊断测试结果:
|
||
- **s3.1080p.hk 公网下载速度**:20-33 KB/s(frp内网穿透,很慢)
|
||
- **s3.yydd.ai 公网下载速度**:类似(也是frp)
|
||
- **内网速度**:服务器在内网,速度快得多
|
||
|
||
### 迁移方式对比
|
||
|
||
#### 方案1:通过服务器内网中转(推荐)
|
||
**流程**:s3.1080p.hk → 192.168.58.20服务器 → s3.yydd.ai
|
||
|
||
**优点**:
|
||
- 服务器在内网,下载/上传速度快
|
||
- 可以并发处理多个文件
|
||
- 可以断点续传
|
||
|
||
**预计时间**:
|
||
- 假设内网速度:10 MB/s
|
||
- 数据量:1.28 GB
|
||
- 下载时间:1.28 GB ÷ 10 MB/s = **约 2分钟**
|
||
- 上传时间:1.28 GB ÷ 10 MB/s = **约 2分钟**
|
||
- **总计:约 4分钟**(理想情况)
|
||
|
||
**实际时间**:
|
||
- 考虑并发处理、网络波动、错误重试
|
||
- **预计:10-20分钟**
|
||
|
||
#### 方案2:直接S3间复制
|
||
**流程**:s3.1080p.hk → s3.yydd.ai(直接复制)
|
||
|
||
**缺点**:
|
||
- 两个S3可能不支持服务端复制
|
||
- 可能走公网,速度慢
|
||
- **不推荐**
|
||
|
||
---
|
||
|
||
## 迁移方案(推荐方案1)
|
||
|
||
### 迁移步骤
|
||
|
||
#### 1. 准备阶段
|
||
- [x] 统计需要迁移的文件数量和大小
|
||
- [x] 了解项目存储结构
|
||
- [ ] 备份数据库(防止迁移失败)
|
||
- [ ] 创建迁移脚本
|
||
- [ ] 在影子系统测试迁移脚本
|
||
|
||
#### 2. 迁移脚本功能
|
||
需要迁移的字段:
|
||
- `previewUrl` - 预览图
|
||
- `zipFileName` - 音频文件(仅当包含S3 URL时)
|
||
- `displayImages` - 详情图数组
|
||
- `videoSourceUrl` - 视频源文件
|
||
- `videoPreview540Url` - 540P预览视频
|
||
- `videoPreviewClipUrl` - 视频片段
|
||
- `videoCoverUrl` - 视频封面
|
||
|
||
脚本逻辑:
|
||
- 从数据库查询所有包含 `s3.1080p.hk` 的素材
|
||
- 遍历每个素材的每个S3字段:
|
||
- 下载文件到服务器临时目录
|
||
- 上传到 `s3.yydd.ai`
|
||
- 更新数据库中的URL
|
||
- 记录迁移日志(成功/失败)
|
||
- 支持断点续传(失败后可继续)
|
||
|
||
#### 3. 迁移执行
|
||
- 在影子系统先测试(少量素材)
|
||
- 验证迁移后的素材可正常访问
|
||
- 在正式系统执行完整迁移
|
||
- 迁移过程中系统可正常使用(只读)
|
||
|
||
#### 4. 验证阶段
|
||
- 检查数据库中所有URL已更新为 `s3.yydd.ai`
|
||
- 随机抽查素材可正常访问
|
||
- 检查详情图、视频预览可正常显示
|
||
- 确认数据量一致
|
||
|
||
#### 5. 清理阶段
|
||
- 确认迁移成功后,可选择删除 `s3.1080p.hk` 上的旧文件
|
||
- 更新 `.env` 配置,统一使用 `s3.yydd.ai`
|
||
- 重新部署应用
|
||
|
||
---
|
||
|
||
## 迁移脚本设计
|
||
|
||
### 脚本功能
|
||
```typescript
|
||
// scripts/migrate-s3-materials.ts
|
||
|
||
interface MigrationResult {
|
||
success: number;
|
||
failed: number;
|
||
skipped: number;
|
||
errors: Array<{ materialId: string; field: string; error: string }>;
|
||
}
|
||
|
||
// 需要迁移的S3字段
|
||
const S3_FIELDS = [
|
||
'previewUrl',
|
||
'zipFileName', // 仅当包含S3 URL时
|
||
'videoSourceUrl',
|
||
'videoPreview540Url',
|
||
'videoPreviewClipUrl',
|
||
'videoCoverUrl',
|
||
];
|
||
|
||
async function migrateMaterials() {
|
||
// 1. 查询所有需要迁移的素材(任意字段包含老S3 URL)
|
||
const materials = await prisma.$queryRaw`
|
||
SELECT * FROM materials
|
||
WHERE "deletedAt" IS NULL
|
||
AND (
|
||
"previewUrl" LIKE '%s3.1080p.hk%'
|
||
OR "zipFileName" LIKE '%s3.1080p.hk%'
|
||
OR "videoSourceUrl" LIKE '%s3.1080p.hk%'
|
||
OR "videoPreview540Url" LIKE '%s3.1080p.hk%'
|
||
OR "videoPreviewClipUrl" LIKE '%s3.1080p.hk%'
|
||
OR "videoCoverUrl" LIKE '%s3.1080p.hk%'
|
||
OR EXISTS (
|
||
SELECT 1 FROM unnest("displayImages") AS url
|
||
WHERE url LIKE '%s3.1080p.hk%'
|
||
)
|
||
)
|
||
`;
|
||
|
||
// 2. 遍历迁移
|
||
for (const material of materials) {
|
||
try {
|
||
const updates: Record<string, string> = {};
|
||
|
||
// 2.1 迁移各个S3字段
|
||
for (const field of S3_FIELDS) {
|
||
const url = material[field];
|
||
if (url && url.includes('s3.1080p.hk')) {
|
||
const newUrl = await migrateFile(url);
|
||
updates[field] = newUrl;
|
||
}
|
||
}
|
||
|
||
// 2.2 迁移详情图数组
|
||
let newDisplayImages = material.displayImages;
|
||
if (material.displayImages && material.displayImages.length > 0) {
|
||
newDisplayImages = [];
|
||
for (const imageUrl of material.displayImages) {
|
||
if (imageUrl.includes('s3.1080p.hk')) {
|
||
const newImageUrl = await migrateFile(imageUrl);
|
||
newDisplayImages.push(newImageUrl);
|
||
} else {
|
||
newDisplayImages.push(imageUrl);
|
||
}
|
||
}
|
||
updates.displayImages = newDisplayImages;
|
||
}
|
||
|
||
// 2.3 更新数据库
|
||
if (Object.keys(updates).length > 0) {
|
||
await prisma.material.update({
|
||
where: { id: material.id },
|
||
data: updates
|
||
});
|
||
}
|
||
|
||
console.log(`✅ Migrated: ${material.id}`);
|
||
} catch (error) {
|
||
console.error(`❌ Failed: ${material.id}`, error);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function migrateFile(oldUrl: string): Promise<string> {
|
||
// 1. 从旧S3下载文件
|
||
const response = await fetch(oldUrl);
|
||
const buffer = await response.arrayBuffer();
|
||
|
||
// 2. 提取文件名和类型
|
||
const fileName = extractFileName(oldUrl);
|
||
const contentType = getContentType(fileName);
|
||
|
||
// 3. 上传到新S3
|
||
const newUrl = await uploadToNewS3(Buffer.from(buffer), fileName, contentType);
|
||
|
||
return newUrl;
|
||
}
|
||
```
|
||
|
||
### 并发控制
|
||
```typescript
|
||
// 使用 p-limit 控制并发数
|
||
import pLimit from 'p-limit';
|
||
|
||
const limit = pLimit(5); // 同时处理5个文件(避免内存溢出)
|
||
|
||
const promises = materials.map(material =>
|
||
limit(() => migrateMaterial(material))
|
||
);
|
||
|
||
await Promise.all(promises);
|
||
```
|
||
|
||
---
|
||
|
||
## 风险评估
|
||
|
||
### 高风险
|
||
- ❌ **数据丢失**:迁移过程中文件丢失
|
||
- 缓解措施:先备份数据库,迁移后验证
|
||
|
||
- ❌ **URL更新失败**:数据库更新失败导致URL不一致
|
||
- 缓解措施:使用事务,失败自动回滚
|
||
|
||
### 中风险
|
||
- ⚠️ **迁移时间过长**:影响用户使用
|
||
- 缓解措施:选择低峰期迁移,支持断点续传
|
||
|
||
- ⚠️ **网络不稳定**:下载/上传失败
|
||
- 缓解措施:自动重试机制,记录失败日志
|
||
|
||
### 低风险
|
||
- ℹ️ **旧文件残留**:迁移后旧文件未删除
|
||
- 缓解措施:迁移成功后手动清理
|
||
|
||
---
|
||
|
||
## 回滚方案
|
||
|
||
如果迁移失败,需要回滚:
|
||
|
||
### 回滚步骤
|
||
1. 停止迁移脚本
|
||
2. 恢复数据库备份
|
||
3. 检查已迁移的素材
|
||
4. 删除新S3上的文件(如果需要)
|
||
5. 重新执行迁移(修复问题后)
|
||
|
||
### 回滚SQL
|
||
```sql
|
||
-- 恢复备份(如果有)
|
||
-- 或者手动更新URL
|
||
UPDATE materials
|
||
SET
|
||
"previewUrl" = REPLACE("previewUrl", 's3.yydd.ai/gongxiangpan', 's3.1080p.hk/jsxiangche'),
|
||
"displayImages" = ARRAY(
|
||
SELECT REPLACE(url::text, 's3.yydd.ai/gongxiangpan', 's3.1080p.hk/jsxiangche')
|
||
FROM unnest("displayImages") AS url
|
||
)
|
||
WHERE "previewUrl" LIKE '%s3.yydd.ai%';
|
||
```
|
||
|
||
---
|
||
|
||
## 迁移后配置更新
|
||
|
||
### 1. 更新 .env 配置
|
||
```bash
|
||
# 旧配置(s3.1080p.hk)
|
||
S3_ENDPOINT=https://s3.1080p.hk
|
||
S3_BUCKET=jsxiangche
|
||
S3_PUBLIC_DOMAIN=https://s3.1080p.hk/jsxiangche
|
||
|
||
# 新配置(s3.yydd.ai)
|
||
S3_ENDPOINT=https://s3.yydd.ai
|
||
S3_BUCKET=gongxiangpan
|
||
S3_PUBLIC_DOMAIN=https://s3.yydd.ai/gongxiangpan
|
||
S3_FILE_PATH=由壹点AI/
|
||
```
|
||
|
||
### 2. 需要确认的新S3凭证
|
||
迁移前需要确认 `s3.yydd.ai` 的访问凭证:
|
||
- S3_ACCESS_KEY_ID
|
||
- S3_SECRET_ACCESS_KEY
|
||
|
||
### 3. 重新部署应用
|
||
```bash
|
||
# 部署到影子系统测试
|
||
.\deploy-local-build.ps1 -Target shadow
|
||
|
||
# 验证成功后部署到正式系统
|
||
.\deploy-local-build.ps1
|
||
```
|
||
|
||
---
|
||
|
||
## 执行计划
|
||
|
||
### 时间安排
|
||
- **准备阶段**:30分钟(创建脚本、配置凭证)
|
||
- **影子系统测试**:10分钟(迁移少量素材测试)
|
||
- **正式系统迁移**:10-20分钟(迁移全部素材)
|
||
- **验证阶段**:10分钟(检查数据完整性)
|
||
- **总计**:约 1小时
|
||
|
||
### 执行时间建议
|
||
- 选择低峰期(晚上或周末)
|
||
- 提前通知用户(如果需要)
|
||
- 准备回滚方案
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
### 迁移数据量
|
||
- **文件数量**:3,895个文件
|
||
- **数据大小**:约 1.28 GB
|
||
- **预计时间**:10-20分钟(实际迁移)
|
||
|
||
### 文件类型分布
|
||
| 类型 | 文件数 | 大小 |
|
||
|------|--------|------|
|
||
| 音频/视频源文件 | 101 | 706 MB |
|
||
| 预览图 | 1,114 | 56 MB |
|
||
| 详情图 | 2,358 | 236 MB |
|
||
| 视频转码文件 | 322 | 281 MB |
|
||
| **总计** | **3,895** | **约 1.28 GB** |
|
||
|
||
### 推荐方案
|
||
- 使用方案1(通过服务器内网中转)
|
||
- 并发处理(5个文件同时)
|
||
- 支持断点续传
|
||
- 完整的日志记录
|
||
|
||
### 下一步
|
||
1. 确认 `s3.yydd.ai` 的访问凭证
|
||
2. 创建迁移脚本
|
||
3. 在影子系统测试
|
||
4. 执行正式迁移
|
||
5. 验证数据完整性
|
||
6. 更新配置并重新部署
|