gongxiangp2/scripts/restore-missing-audio-files.js

220 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
/**
* 恢复缺失的音频文件脚本
*
* 目标:将 S3 上的音频文件下载到本地 uploads 目录,并更新数据库
*
* 说明:
* - 处理所有音乐类型的素材
* - 检查本地文件是否存在,如果不存在则尝试从 S3 恢复
* - 由于原始的 S3 URL 可能已丢失,会尝试匹配 S3 上的所有音频文件
*
* ⚠️ 重要说明:
* - 本脚本仅从 S3 下载文件到本地,不会删除 S3 上的任何文件
* - S3 存储桶内的源文件将完整保留,确保迁移失败时源文件不丢失
* - 如果需要清理 S3 文件,请在确认迁移完全成功后再手动执行
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const client_s3_1 = require("@aws-sdk/client-s3");
const promises_1 = require("fs/promises");
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const prisma = new client_1.PrismaClient();
// S3 客户端配置
const s3Client = new client_s3_1.S3Client({
endpoint: process.env.S3_ENDPOINT || '',
region: process.env.S3_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
forcePathStyle: true,
requestHandler: {
requestTimeout: 300000, // 5分钟超时
connectionTimeout: 30000, // 30秒连接超时
},
maxAttempts: 5,
retryMode: 'adaptive',
});
const S3_BUCKET = process.env.S3_BUCKET || '';
const S3_PUBLIC_DOMAIN = process.env.S3_PUBLIC_DOMAIN || '';
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
/**
* 从 S3 下载整个对象为 Buffer
*/
async function downloadS3Object(key) {
const cmd = new client_s3_1.GetObjectCommand({
Bucket: S3_BUCKET,
Key: key,
});
const res = await s3Client.send(cmd);
const body = res.Body;
if (!body) {
throw new Error('S3 对象 Body 为空');
}
const chunks = [];
for await (const chunk of body) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
/**
* 列出 S3 上的所有音频文件
*/
async function listS3AudioFiles() {
const audioKeys = [];
let continuationToken = undefined;
do {
const listCommand = new client_s3_1.ListObjectsV2Command({
Bucket: S3_BUCKET,
Prefix: '由壹点AI/',
ContinuationToken: continuationToken,
MaxKeys: 1000,
});
const response = await s3Client.send(listCommand);
const audioFiles = (response.Contents || []).filter((obj) => obj.Key && (obj.Key.endsWith('.mp3') || obj.Key.endsWith('.wav') || obj.Key.endsWith('.m4a')));
audioKeys.push(...audioFiles.map((obj) => obj.Key));
continuationToken = response.NextContinuationToken;
} while (continuationToken);
return audioKeys;
}
/**
* 恢复一个音频文件
*/
async function restoreOneAudio(materialId, currentFileName, s3Key) {
const localFileName = currentFileName; // 使用数据库中的文件名
const localFullPath = path_1.default.join(UPLOAD_DIR, localFileName);
// 如果文件已存在,跳过
if ((0, fs_1.existsSync)(localFullPath)) {
console.log(` ✅ 文件已存在: ${localFileName}`);
return true;
}
try {
console.log(` 📥 从 S3 下载: ${s3Key}`);
const buffer = await downloadS3Object(s3Key);
console.log(` 📦 下载完成,大小:${buffer.length} 字节`);
// 确保 uploads 目录存在
if (!(0, fs_1.existsSync)(UPLOAD_DIR)) {
await (0, promises_1.mkdir)(UPLOAD_DIR, { recursive: true });
}
// 写入本地(使用数据库中的文件名)
console.log(` 💾 写入本地文件: ${localFullPath}`);
await (0, promises_1.writeFile)(localFullPath, buffer);
// 校验
const st = await (0, promises_1.stat)(localFullPath);
if (st.size !== buffer.length) {
throw new Error(`本地文件大小不匹配,期望 ${buffer.length} 实际 ${st.size}`);
}
// 更新数据库的 zipFilePath(使用 Linux 路径分隔符)
const zipFilePath = localFullPath.replace(/\\/g, '/');
await prisma.material.update({
where: { id: materialId },
data: {
zipFilePath: zipFilePath,
},
});
console.log(` ✅ 恢复完成: ${localFileName}`);
return true;
}
catch (error) {
console.error(` ❌ 恢复失败: ${error.message}`);
return false;
}
}
async function main() {
console.log('🚀 开始恢复缺失的音频文件...\n');
if (!S3_BUCKET || !S3_PUBLIC_DOMAIN) {
console.error('❌ 缺少必要环境变量:S3_BUCKET 或 S3_PUBLIC_DOMAIN');
process.exit(1);
}
console.log('📋 配置信息:');
console.log(` S3_BUCKET: ${S3_BUCKET}`);
console.log(` S3_PUBLIC_DOMAIN: ${S3_PUBLIC_DOMAIN}`);
console.log(` UPLOAD_DIR: ${UPLOAD_DIR}`);
console.log('');
// 查询所有音乐素材
const materials = await prisma.material.findMany({
where: {
deletedAt: null,
materialType: '音乐',
},
select: {
id: true,
zipFileName: true,
zipFilePath: true,
},
orderBy: { createdAt: 'asc' },
});
console.log(`🔍 找到 ${materials.length} 个音乐素材\n`);
// 列出 S3 上的所有音频文件
console.log('📋 列出 S3 上的音频文件...');
const s3AudioKeys = await listS3AudioFiles();
console.log(`📦 S3 上共有 ${s3AudioKeys.length} 个音频文件\n`);
if (s3AudioKeys.length === 0) {
console.log('❌ S3 上没有音频文件');
return;
}
// 统计需要恢复的文件
let restored = 0;
let skipped = 0;
let failed = 0;
// 简单匹配策略:按照创建时间顺序匹配
// 由于无法精确匹配,我们将按照顺序分配 S3 文件给缺失文件的素材
const missingMaterials = materials.filter(m => {
if (!m.zipFileName)
return false;
const localPath = path_1.default.join(UPLOAD_DIR, m.zipFileName);
return !(0, fs_1.existsSync)(localPath);
});
console.log(`📊 需要恢复的文件数: ${missingMaterials.length}\n`);
if (missingMaterials.length === 0) {
console.log('✅ 所有文件都已存在,无需恢复');
return;
}
// 如果缺失的文件数超过 S3 上的文件数,只处理能匹配的部分
const maxToRestore = Math.min(missingMaterials.length, s3AudioKeys.length);
console.log(`⚠️ 注意:将按照顺序匹配前 ${maxToRestore} 个文件\n`);
for (let i = 0; i < maxToRestore; i++) {
const material = missingMaterials[i];
const s3Key = s3AudioKeys[i];
console.log(`\n==== [${i + 1}/${maxToRestore}] 恢复素材 ${material.id} ====`);
console.log(` 当前文件名: ${material.zipFileName}`);
console.log(` S3 Key: ${s3Key}`);
try {
const success = await restoreOneAudio(material.id, material.zipFileName, s3Key);
if (success) {
restored++;
}
else {
failed++;
}
}
catch (error) {
failed++;
console.error(` ❌ 恢复失败: ${error.message}`);
}
}
console.log('\n📊 恢复结果统计:');
console.log(` 成功恢复: ${restored}`);
console.log(` 跳过: ${skipped}`);
console.log(` 失败: ${failed}`);
if (restored > 0) {
console.log('\n✅ 音频文件恢复完成');
}
else {
console.log('\n⚠️ 没有成功恢复任何文件');
}
}
main()
.catch((err) => {
console.error('❌ 脚本执行出错:', err);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});