250 lines
7.9 KiB
TypeScript
250 lines
7.9 KiB
TypeScript
/**
|
||
* 检查S3文件是否存在
|
||
* 用于诊断预览图加载失败问题
|
||
*
|
||
* 使用方法:
|
||
* npx tsx scripts/check-s3-file.ts <url>
|
||
*
|
||
* 示例:
|
||
* npx tsx scripts/check-s3-file.ts "http://192.168.58.20:9000/jsxiangche/%E7%94%B1%E5%A3%B9%E7%82%B9AI/video-cover-3e9897ae-8844-446a-a5d6-fb78cf56bc38-1766384242637.jpg"
|
||
*/
|
||
|
||
import { S3Client, HeadObjectCommand, ListBucketsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
||
|
||
// 从环境变量获取配置
|
||
const S3_ENDPOINT = process.env.S3_ENDPOINT || 'http://192.168.58.20:9000';
|
||
const S3_BUCKET = process.env.S3_BUCKET || 'gongxiangpan';
|
||
const S3_ACCESS_KEY_ID = process.env.S3_ACCESS_KEY_ID || 'GXPAccessKey2025';
|
||
const S3_SECRET_ACCESS_KEY = process.env.S3_SECRET_ACCESS_KEY || 'GXPSecretKey2025SecureMinIO';
|
||
const S3_REGION = process.env.S3_REGION || 'us-east-1';
|
||
|
||
const s3Client = new S3Client({
|
||
endpoint: S3_ENDPOINT,
|
||
region: S3_REGION,
|
||
credentials: {
|
||
accessKeyId: S3_ACCESS_KEY_ID,
|
||
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
||
},
|
||
forcePathStyle: true, // MinIO requires path-style
|
||
});
|
||
|
||
/**
|
||
* 从URL提取bucket名称和key
|
||
*/
|
||
function parseS3Url(url: string): { bucket: string; key: string } | null {
|
||
try {
|
||
const urlObj = new URL(url);
|
||
const pathParts = urlObj.pathname.split('/').filter(p => p);
|
||
|
||
if (pathParts.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
// MinIO path-style: /bucket-name/key
|
||
const bucket = decodeURIComponent(pathParts[0]);
|
||
const key = pathParts.slice(1).map(p => decodeURIComponent(p)).join('/');
|
||
|
||
return { bucket, key };
|
||
} catch (error) {
|
||
console.error('解析URL失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查文件是否存在
|
||
*/
|
||
async function checkFileExists(bucket: string, key: string): Promise<boolean> {
|
||
try {
|
||
const command = new HeadObjectCommand({
|
||
Bucket: bucket,
|
||
Key: key,
|
||
});
|
||
|
||
await s3Client.send(command);
|
||
return true;
|
||
} catch (error: any) {
|
||
if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) {
|
||
return false;
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 列出所有bucket
|
||
*/
|
||
async function listBuckets(): Promise<string[]> {
|
||
try {
|
||
const command = new ListBucketsCommand({});
|
||
const response = await s3Client.send(command);
|
||
return response.Buckets?.map(b => b.Name || '') || [];
|
||
} catch (error) {
|
||
console.error('列出bucket失败:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 列出bucket中的对象(限制10个)
|
||
*/
|
||
async function listObjects(bucket: string, prefix?: string): Promise<string[]> {
|
||
try {
|
||
const command = new ListObjectsV2Command({
|
||
Bucket: bucket,
|
||
Prefix: prefix,
|
||
MaxKeys: 10,
|
||
});
|
||
|
||
const response = await s3Client.send(command);
|
||
return response.Contents?.map(obj => obj.Key || '') || [];
|
||
} catch (error) {
|
||
console.error('列出对象失败:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
const url = process.argv[2];
|
||
|
||
if (!url) {
|
||
console.log('使用方法: npx tsx scripts/check-s3-file.ts <url>');
|
||
console.log('示例: npx tsx scripts/check-s3-file.ts "http://192.168.58.20:9000/jsxiangche/由壹点AI/video-cover-xxx.jpg"');
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log('='.repeat(60));
|
||
console.log('S3文件检查工具');
|
||
console.log('='.repeat(60));
|
||
console.log();
|
||
|
||
console.log('配置信息:');
|
||
console.log(` S3_ENDPOINT: ${S3_ENDPOINT}`);
|
||
console.log(` S3_BUCKET: ${S3_BUCKET}`);
|
||
console.log(` S3_REGION: ${S3_REGION}`);
|
||
console.log();
|
||
|
||
// 1. 列出所有bucket
|
||
console.log('1. 检查可用的bucket...');
|
||
const buckets = await listBuckets();
|
||
console.log(` 可用bucket: ${buckets.length > 0 ? buckets.join(', ') : '无'}`);
|
||
console.log();
|
||
|
||
// 2. 解析URL
|
||
console.log('2. 解析URL...');
|
||
console.log(` URL: ${url}`);
|
||
const parsed = parseS3Url(url);
|
||
if (!parsed) {
|
||
console.error(' ❌ URL格式错误,无法解析');
|
||
process.exit(1);
|
||
}
|
||
console.log(` Bucket: ${parsed.bucket}`);
|
||
console.log(` Key: ${parsed.key}`);
|
||
console.log();
|
||
|
||
// 3. 检查bucket是否存在
|
||
console.log('3. 检查bucket是否存在...');
|
||
if (!buckets.includes(parsed.bucket)) {
|
||
console.log(` ❌ Bucket "${parsed.bucket}" 不存在`);
|
||
console.log(` 可用bucket: ${buckets.join(', ')}`);
|
||
console.log();
|
||
|
||
// 尝试在新bucket中查找(可能有不同的key路径)
|
||
if (parsed.bucket === 'jsxiangche' && buckets.includes(S3_BUCKET)) {
|
||
console.log(` 提示: 尝试在新bucket "${S3_BUCKET}" 中查找...`);
|
||
|
||
// 尝试多种可能的key路径
|
||
const possibleKeys = [
|
||
parsed.key, // 原始key
|
||
`由壹点AI/${parsed.key}`, // 添加路径前缀
|
||
parsed.key.split('/').slice(1).join('/'), // 移除第一个路径段
|
||
`由壹点AI/${parsed.key.split('/').slice(1).join('/')}`, // 移除第一个路径段并添加前缀
|
||
];
|
||
|
||
let found = false;
|
||
for (const testKey of possibleKeys) {
|
||
const exists = await checkFileExists(S3_BUCKET, testKey);
|
||
if (exists) {
|
||
console.log(` ✅ 文件存在于新bucket "${S3_BUCKET}" 中`);
|
||
console.log(` Key: ${testKey}`);
|
||
console.log(` 新URL: ${S3_ENDPOINT}/${S3_BUCKET}/${testKey}`);
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!found) {
|
||
console.log(` ❌ 文件在新bucket "${S3_BUCKET}" 中不存在(尝试了 ${possibleKeys.length} 种key路径)`);
|
||
|
||
// 列出bucket中的一些文件(用于调试)
|
||
console.log(` 列出bucket "${S3_BUCKET}" 中的部分文件...`);
|
||
const objects = await listObjects(S3_BUCKET, '由壹点AI/video-cover');
|
||
if (objects.length > 0) {
|
||
console.log(` 找到 ${objects.length} 个文件(前缀: 由壹点AI/video-cover):`);
|
||
objects.slice(0, 5).forEach(obj => {
|
||
console.log(` - ${obj}`);
|
||
});
|
||
} else {
|
||
console.log(` 没有找到文件(前缀: 由壹点AI/video-cover)`);
|
||
}
|
||
}
|
||
}
|
||
process.exit(1);
|
||
}
|
||
console.log(` ✅ Bucket "${parsed.bucket}" 存在`);
|
||
console.log();
|
||
|
||
// 4. 检查文件是否存在
|
||
console.log('4. 检查文件是否存在...');
|
||
try {
|
||
const exists = await checkFileExists(parsed.bucket, parsed.key);
|
||
if (exists) {
|
||
console.log(` ✅ 文件存在`);
|
||
console.log();
|
||
|
||
// 5. 尝试通过HTTP访问
|
||
console.log('5. 测试HTTP访问...');
|
||
try {
|
||
const response = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(5000) });
|
||
if (response.ok) {
|
||
console.log(` ✅ HTTP访问成功 (${response.status})`);
|
||
console.log(` Content-Type: ${response.headers.get('content-type')}`);
|
||
console.log(` Content-Length: ${response.headers.get('content-length')}`);
|
||
} else {
|
||
console.log(` ❌ HTTP访问失败 (${response.status} ${response.statusText})`);
|
||
}
|
||
} catch (fetchError: any) {
|
||
console.log(` ❌ HTTP访问失败: ${fetchError.message}`);
|
||
}
|
||
} else {
|
||
console.log(` ❌ 文件不存在`);
|
||
console.log();
|
||
|
||
// 列出bucket中的一些文件(用于调试)
|
||
console.log('6. 列出bucket中的部分文件...');
|
||
const objects = await listObjects(parsed.bucket, parsed.key.split('/')[0]);
|
||
if (objects.length > 0) {
|
||
console.log(` 找到 ${objects.length} 个文件(前缀: ${parsed.key.split('/')[0]}):`);
|
||
objects.slice(0, 5).forEach(obj => {
|
||
console.log(` - ${obj}`);
|
||
});
|
||
} else {
|
||
console.log(` 没有找到文件(前缀: ${parsed.key.split('/')[0]})`);
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error(` ❌ 检查文件时出错: ${error.message}`);
|
||
if (error.$metadata) {
|
||
console.error(` HTTP状态码: ${error.$metadata.httpStatusCode}`);
|
||
}
|
||
}
|
||
|
||
console.log();
|
||
console.log('='.repeat(60));
|
||
}
|
||
|
||
main().catch(error => {
|
||
console.error('执行失败:', error);
|
||
process.exit(1);
|
||
});
|