12 KiB
12 KiB
S3素材统一迁移方案
背景
当前系统中素材分散在两个S3存储位置:
- 老位置:
s3.1080p.hk/jsxiangche- 老素材 - 新位置:
s3.yydd.ai/gongxiangpan- 新素材
目标:将所有素材统一迁移到新位置 s3.yydd.ai/gongxiangpan,方便后期维护。
项目存储结构
文件存储方式
- 源文件(压缩包) → 本地
./uploads目录(Docker挂载)- 不需要迁移(1,411个,699.5 GB) - 音频/视频源文件 → MinIO S3(zipFileName字段)- 需要迁移(97个在老S3)
- 预览图/视频转码文件 → 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. 准备阶段
- 统计需要迁移的文件数量和大小
- 了解项目存储结构
- 备份数据库(防止迁移失败)
- 创建迁移脚本
- 在影子系统测试迁移脚本
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 - 重新部署应用
迁移脚本设计
脚本功能
// 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;
}
并发控制
// 使用 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不一致
- 缓解措施:使用事务,失败自动回滚
中风险
-
⚠️ 迁移时间过长:影响用户使用
- 缓解措施:选择低峰期迁移,支持断点续传
-
⚠️ 网络不稳定:下载/上传失败
- 缓解措施:自动重试机制,记录失败日志
低风险
- ℹ️ 旧文件残留:迁移后旧文件未删除
- 缓解措施:迁移成功后手动清理
回滚方案
如果迁移失败,需要回滚:
回滚步骤
- 停止迁移脚本
- 恢复数据库备份
- 检查已迁移的素材
- 删除新S3上的文件(如果需要)
- 重新执行迁移(修复问题后)
回滚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 配置
# 旧配置(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. 重新部署应用
# 部署到影子系统测试
.\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个文件同时)
- 支持断点续传
- 完整的日志记录
下一步
- 确认
s3.yydd.ai的访问凭证 - 创建迁移脚本
- 在影子系统测试
- 执行正式迁移
- 验证数据完整性
- 更新配置并重新部署