302 lines
9.9 KiB
TypeScript
302 lines
9.9 KiB
TypeScript
/**
|
||
* 全面检查素材文件是否存在
|
||
* 用于诊断预览图加载失败问题
|
||
*
|
||
* 检查:
|
||
* 1. 旧MinIO服务器(s3.1080p.hk)上是否存在
|
||
* 2. 新MinIO服务器(当前MinIO)上是否存在
|
||
* 3. 文件是否真的不存在
|
||
*
|
||
* 使用方法:
|
||
* npx tsx scripts/check-material-files-comprehensive.ts <materialId>
|
||
*
|
||
* 示例:
|
||
* npx tsx scripts/check-material-files-comprehensive.ts "3e9897ae-8844-446a-a5d6-fb78cf56bc38"
|
||
*/
|
||
|
||
import { PrismaClient } from '@prisma/client';
|
||
import { S3Client, HeadObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
||
|
||
const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://gongxiangpan:gongxiangpan123@192.168.58.20:5432/lobechat';
|
||
const prisma = new PrismaClient({
|
||
datasources: {
|
||
db: {
|
||
url: DATABASE_URL,
|
||
},
|
||
},
|
||
});
|
||
|
||
// 当前MinIO配置
|
||
const CURRENT_S3_ENDPOINT = process.env.S3_ENDPOINT || 'http://192.168.58.20:9000';
|
||
const CURRENT_S3_BUCKET = process.env.S3_BUCKET || 'gongxiangpan';
|
||
const CURRENT_S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID || 'GXPAccessKey2025';
|
||
const CURRENT_S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY || 'GXPSecretKey2025SecureMinIO';
|
||
const CURRENT_S3_REGION = process.env.S3_REGION || 'us-east-1';
|
||
|
||
// 旧MinIO配置(如果存在)
|
||
const OLD_S3_ENDPOINT = 'https://s3.1080p.hk';
|
||
const OLD_S3_BUCKET = 'jsxiangche';
|
||
const OLD_S3_ACCESS_KEY_ID = process.env.OLD_S3_ACCESS_KEY_ID || '';
|
||
const OLD_S3_SECRET_ACCESS_KEY = process.env.OLD_S3_SECRET_ACCESS_KEY || '';
|
||
|
||
// 创建S3客户端
|
||
const currentS3Client = new S3Client({
|
||
endpoint: CURRENT_S3_ENDPOINT,
|
||
region: CURRENT_S3_REGION,
|
||
credentials: {
|
||
accessKeyId: CURRENT_S3_ACCESS_KEY_ID,
|
||
secretAccessKey: CURRENT_S3_SECRET_ACCESS_KEY,
|
||
},
|
||
forcePathStyle: true,
|
||
});
|
||
|
||
// 旧S3客户端(如果配置了凭据)
|
||
let oldS3Client: S3Client | null = null;
|
||
if (OLD_S3_ACCESS_KEY_ID && OLD_S3_SECRET_ACCESS_KEY) {
|
||
oldS3Client = new S3Client({
|
||
endpoint: OLD_S3_ENDPOINT,
|
||
region: 'us-east-1',
|
||
credentials: {
|
||
accessKeyId: OLD_S3_ACCESS_KEY_ID,
|
||
secretAccessKey: OLD_S3_SECRET_ACCESS_KEY,
|
||
},
|
||
forcePathStyle: true,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 从URL提取bucket和key
|
||
*/
|
||
function parseUrl(url: string): { bucket: string; key: string; domain: string } | null {
|
||
try {
|
||
const urlObj = new URL(url);
|
||
const pathParts = urlObj.pathname.split('/').filter(p => p);
|
||
if (pathParts.length === 0) return null;
|
||
const bucket = decodeURIComponent(pathParts[0]);
|
||
const key = pathParts.slice(1).map(p => decodeURIComponent(p)).join('/');
|
||
return { bucket, key, domain: urlObj.hostname };
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查文件是否存在
|
||
*/
|
||
async function checkFileExists(
|
||
client: S3Client,
|
||
bucket: string,
|
||
key: string,
|
||
endpoint: string
|
||
): Promise<{ exists: boolean; error?: string }> {
|
||
try {
|
||
const command = new HeadObjectCommand({
|
||
Bucket: bucket,
|
||
Key: key,
|
||
});
|
||
await client.send(command);
|
||
return { exists: true };
|
||
} catch (error: any) {
|
||
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
|
||
return { exists: false };
|
||
}
|
||
return { exists: false, error: error.message || String(error) };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 列出bucket中的对象(用于调试)
|
||
*/
|
||
async function listObjects(client: S3Client, bucket: string, prefix: string, limit: number = 5): Promise<string[]> {
|
||
try {
|
||
const command = new ListObjectsV2Command({
|
||
Bucket: bucket,
|
||
Prefix: prefix,
|
||
MaxKeys: limit,
|
||
});
|
||
const response = await client.send(command);
|
||
return response.Contents?.map(obj => obj.Key || '') || [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
const materialId = process.argv[2];
|
||
|
||
if (!materialId) {
|
||
console.log('使用方法: npx tsx scripts/check-material-files-comprehensive.ts <materialId>');
|
||
console.log('示例: npx tsx scripts/check-material-files-comprehensive.ts "3e9897ae-8844-446a-a5d6-fb78cf56bc38"');
|
||
process.exit(1);
|
||
}
|
||
|
||
try {
|
||
// 1. 从数据库获取素材信息
|
||
console.log('='.repeat(60));
|
||
console.log('全面文件检查');
|
||
console.log('='.repeat(60));
|
||
console.log();
|
||
|
||
const material = await prisma.material.findUnique({
|
||
where: { id: materialId },
|
||
select: {
|
||
id: true,
|
||
productModel: true,
|
||
materialType: true,
|
||
createdAt: true,
|
||
previewUrl: true,
|
||
videoCoverUrl: true,
|
||
videoPreview540Url: true,
|
||
videoPreviewClipUrl: true,
|
||
} as any,
|
||
});
|
||
|
||
if (!material) {
|
||
console.log(`❌ 素材不存在: ${materialId}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log('素材信息:');
|
||
console.log(` ID: ${material.id}`);
|
||
console.log(` 产品型号: ${material.productModel}`);
|
||
console.log(` 素材类型: ${material.materialType}`);
|
||
console.log(` 创建时间: ${material.createdAt}`);
|
||
console.log();
|
||
|
||
// 2. 检查各个URL
|
||
const urlFields = [
|
||
{ name: 'previewUrl', url: (material as any).previewUrl },
|
||
{ name: 'videoCoverUrl', url: (material as any).videoCoverUrl },
|
||
{ name: 'videoPreview540Url', url: (material as any).videoPreview540Url },
|
||
{ name: 'videoPreviewClipUrl', url: (material as any).videoPreviewClipUrl },
|
||
];
|
||
|
||
for (const field of urlFields) {
|
||
if (!field.url) {
|
||
console.log(`${field.name}: (无)`);
|
||
console.log();
|
||
continue;
|
||
}
|
||
|
||
console.log(`${field.name}:`);
|
||
console.log(` URL: ${field.url}`);
|
||
|
||
const parsed = parseUrl(field.url);
|
||
if (!parsed) {
|
||
console.log(` ❌ URL格式错误,无法解析`);
|
||
console.log();
|
||
continue;
|
||
}
|
||
|
||
console.log(` Bucket: ${parsed.bucket}`);
|
||
console.log(` Key: ${parsed.key}`);
|
||
console.log(` Domain: ${parsed.domain}`);
|
||
console.log();
|
||
|
||
// 3. 检查旧MinIO服务器(如果URL指向旧服务器)
|
||
if (parsed.domain.includes('1080p.hk') || parsed.bucket === 'jsxiangche') {
|
||
console.log(` 检查1: 旧MinIO服务器 (${OLD_S3_ENDPOINT})...`);
|
||
if (oldS3Client) {
|
||
const result = await checkFileExists(oldS3Client, parsed.bucket, parsed.key, OLD_S3_ENDPOINT);
|
||
if (result.exists) {
|
||
console.log(` ✅ 文件存在于旧MinIO服务器`);
|
||
} else {
|
||
console.log(` ❌ 文件不存在于旧MinIO服务器`);
|
||
if (result.error) {
|
||
console.log(` 错误: ${result.error}`);
|
||
}
|
||
}
|
||
} else {
|
||
console.log(` ⚠️ 未配置旧MinIO凭据,无法检查`);
|
||
console.log(` 提示: 设置环境变量 OLD_S3_ACCESS_KEY_ID 和 OLD_S3_SECRET_ACCESS_KEY`);
|
||
}
|
||
console.log();
|
||
}
|
||
|
||
// 4. 检查新MinIO服务器(当前MinIO)
|
||
console.log(` 检查2: 新MinIO服务器 (${CURRENT_S3_ENDPOINT})...`);
|
||
|
||
// 尝试原始key
|
||
let found = false;
|
||
const possibleKeys = [
|
||
parsed.key, // 原始key(包含路径前缀)
|
||
parsed.key.replace(/^由壹点AI\//, ''), // 移除路径前缀
|
||
parsed.key.replace(/^jsxiangche\//, ''), // 移除bucket名称(如果key中包含)
|
||
];
|
||
|
||
// 如果URL使用了旧bucket名称,尝试在新bucket中查找
|
||
const testBuckets = parsed.bucket === 'jsxiangche' ? [CURRENT_S3_BUCKET, parsed.bucket] : [parsed.bucket];
|
||
|
||
for (const testBucket of testBuckets) {
|
||
if (found) break;
|
||
for (const testKey of possibleKeys) {
|
||
if (found) break;
|
||
const result = await checkFileExists(currentS3Client, testBucket, testKey, CURRENT_S3_ENDPOINT);
|
||
if (result.exists) {
|
||
console.log(` ✅ 文件存在于新MinIO服务器`);
|
||
console.log(` Bucket: ${testBucket}`);
|
||
console.log(` Key: ${testKey}`);
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!found) {
|
||
console.log(` ❌ 文件不存在于新MinIO服务器`);
|
||
console.log(` 尝试的bucket: ${testBuckets.join(', ')}`);
|
||
console.log(` 尝试的key路径: ${possibleKeys.slice(0, 3).join(', ')}`);
|
||
|
||
// 列出bucket中的一些文件(用于调试)
|
||
if (parsed.key.includes('video-cover') || parsed.key.includes('video-thumb')) {
|
||
const prefix = '由壹点AI/video-cover';
|
||
console.log(` 列出bucket "${CURRENT_S3_BUCKET}" 中的部分文件(前缀: ${prefix})...`);
|
||
const objects = await listObjects(currentS3Client, CURRENT_S3_BUCKET, prefix, 5);
|
||
if (objects.length > 0) {
|
||
console.log(` 找到 ${objects.length} 个文件:`);
|
||
objects.forEach(obj => {
|
||
console.log(` - ${obj}`);
|
||
});
|
||
} else {
|
||
console.log(` 没有找到文件`);
|
||
}
|
||
}
|
||
}
|
||
console.log();
|
||
|
||
// 5. 总结
|
||
console.log(` 总结:`);
|
||
if (parsed.domain.includes('1080p.hk') || parsed.bucket === 'jsxiangche') {
|
||
if (!found) {
|
||
console.log(` ⚠️ URL指向旧MinIO服务器,但文件不在新MinIO服务器中`);
|
||
console.log(` 建议:`);
|
||
console.log(` 1. 检查文件是否在旧MinIO服务器上`);
|
||
console.log(` 2. 如果文件存在,需要迁移到新MinIO服务器`);
|
||
console.log(` 3. 如果文件不存在,URL需要更新或文件需要重新上传`);
|
||
} else {
|
||
console.log(` ✅ 文件在新MinIO服务器中找到(URL需要更新)`);
|
||
}
|
||
} else {
|
||
if (found) {
|
||
console.log(` ✅ 文件存在`);
|
||
} else {
|
||
console.log(` ❌ 文件不存在(可能已被删除)`);
|
||
}
|
||
}
|
||
console.log();
|
||
console.log('-'.repeat(60));
|
||
console.log();
|
||
}
|
||
|
||
console.log('='.repeat(60));
|
||
|
||
} catch (error) {
|
||
console.error('执行失败:', error);
|
||
process.exit(1);
|
||
} finally {
|
||
await prisma.$disconnect();
|
||
}
|
||
}
|
||
|
||
main();
|