diff --git a/scripts/analyze-external-file-sizes.ts b/scripts/analyze-external-file-sizes.ts new file mode 100644 index 0000000..8972760 --- /dev/null +++ b/scripts/analyze-external-file-sizes.ts @@ -0,0 +1,240 @@ +/** + * 分析外网服务器上存在的老素材源文件大小 + */ + +import { readFile, writeFile } from 'fs/promises' + +interface ExternalCheckRow { + 素材ID: string + 素材类型: string + 产品型号: string + 作者: string + 使用店铺: string + 源文件路径: string + 外网服务器URL: string + 文件是否存在: string + 源文件大小: string + 创建时间: string +} + +async function main() { + console.log('='.repeat(60)) + console.log('分析外网服务器上存在的老素材源文件大小') + console.log('='.repeat(60)) + console.log() + + // 读取外网服务器检查报告 + console.log('📋 读取外网服务器检查报告...') + let externalCheckData: ExternalCheckRow[] = [] + try { + const externalCheckContent = await readFile('老素材外网服务器检查报告.csv', 'utf-8') + const lines = externalCheckContent.split('\n').filter(line => line.trim()) + if (lines.length < 2) { + throw new Error('CSV文件格式错误') + } + + // 简单的CSV解析(处理带引号的字段) + function parseCSVLine(line: string): string[] { + const result: string[] = [] + let current = '' + let inQuotes = false + + for (let i = 0; i < line.length; i++) { + const char = line[i] + if (char === '"') { + if (inQuotes && line[i + 1] === '"') { + current += '"' + i++ // 跳过下一个引号 + } else { + inQuotes = !inQuotes + } + } else if (char === ',' && !inQuotes) { + result.push(current) + current = '' + } else { + current += char + } + } + result.push(current) + return result + } + + const headers = parseCSVLine(lines[0]).map(h => h.trim()) + for (let i = 1; i < lines.length; i++) { + const values = parseCSVLine(lines[i]) + const row: any = {} + headers.forEach((header, index) => { + row[header] = (values[index] || '').trim() + }) + externalCheckData.push(row as ExternalCheckRow) + } + console.log(` 读取到 ${externalCheckData.length} 条记录`) + } catch (error) { + console.error('❌ 无法读取外网服务器检查报告:', error) + process.exit(1) + } + + // 筛选出存在的文件 + const existingFiles = externalCheckData.filter(row => row['文件是否存在'] === '是') + const missingFiles = externalCheckData.filter(row => row['文件是否存在'] === '否') + + console.log(` 外网服务器存在: ${existingFiles.length} 个`) + console.log(` 外网服务器缺失: ${missingFiles.length} 个`) + console.log() + + // 分析文件大小 + console.log('📊 分析文件大小...') + + let totalSize = 0 + let totalSizeGB = 0 + const sizeStats: Array<{ + materialId: string + materialType: string + productModel: string + authorName: string + categoryName: string + zipFilePath: string + externalUrl: string + fileSize: number + fileSizeMB: number + fileSizeGB: number + createdAt: string + }> = [] + + for (const row of existingFiles) { + const fileSizeStr = (row['源文件大小(字节)'] || row['源文件大小'] || '0').trim() + const fileSize = parseInt(fileSizeStr, 10) + if (!isNaN(fileSize) && fileSize > 0) { + totalSize += fileSize + const fileSizeMB = fileSize / (1024 * 1024) + const fileSizeGB = fileSize / (1024 * 1024 * 1024) + + sizeStats.push({ + materialId: row['素材ID'], + materialType: row['素材类型'], + productModel: row['产品型号'], + authorName: row['作者'], + categoryName: row['使用店铺'], + zipFilePath: row['源文件路径'], + externalUrl: row['外网服务器URL'], + fileSize, + fileSizeMB: Math.round(fileSizeMB * 100) / 100, + fileSizeGB: Math.round(fileSizeGB * 1000) / 1000, + createdAt: row['创建时间'], + }) + } + } + + totalSizeGB = totalSize / (1024 * 1024 * 1024) + + console.log(` 总文件数: ${sizeStats.length} 个`) + console.log(` 总大小: ${(totalSizeGB).toFixed(2)} GB (${totalSize} 字节)`) + console.log() + + // 按大小排序 + sizeStats.sort((a, b) => b.fileSize - a.fileSize) + + // 统计信息 + const avgSize = totalSize / sizeStats.length + const avgSizeMB = avgSize / (1024 * 1024) + const maxSize = sizeStats[0]?.fileSize || 0 + const minSize = sizeStats[sizeStats.length - 1]?.fileSize || 0 + + console.log('📈 大小统计:') + console.log(` 平均大小: ${(avgSizeMB).toFixed(2)} MB`) + console.log(` 最大文件: ${(maxSize / (1024 * 1024)).toFixed(2)} MB (${sizeStats[0]?.productModel || 'N/A'})`) + console.log(` 最小文件: ${(minSize / (1024 * 1024)).toFixed(2)} MB (${sizeStats[sizeStats.length - 1]?.productModel || 'N/A'})`) + console.log() + + // 按素材类型统计 + const typeStats: Record = {} + for (const stat of sizeStats) { + if (!typeStats[stat.materialType]) { + typeStats[stat.materialType] = { count: 0, totalSize: 0, avgSize: 0 } + } + typeStats[stat.materialType].count++ + typeStats[stat.materialType].totalSize += stat.fileSize + } + + for (const type in typeStats) { + typeStats[type].avgSize = typeStats[type].totalSize / typeStats[type].count + } + + console.log('📦 按素材类型统计:') + for (const [type, stats] of Object.entries(typeStats).sort((a, b) => b[1].totalSize - a[1].totalSize)) { + const totalGB = stats.totalSize / (1024 * 1024 * 1024) + const avgMB = stats.avgSize / (1024 * 1024) + console.log(` ${type}: ${stats.count} 个, 总计 ${totalGB.toFixed(2)} GB, 平均 ${avgMB.toFixed(2)} MB`) + } + console.log() + + // 生成详细报告 + console.log('💾 生成详细报告...') + + const reportRows: string[] = [] + reportRows.push('素材ID,素材类型,产品型号,作者,使用店铺,源文件路径,外网服务器URL,文件大小(字节),文件大小(MB),文件大小(GB),创建时间') + + for (const stat of sizeStats) { + const row = [ + stat.materialId, + stat.materialType, + stat.productModel, + stat.authorName, + stat.categoryName, + stat.zipFilePath, + stat.externalUrl, + stat.fileSize.toString(), + stat.fileSizeMB.toFixed(2), + stat.fileSizeGB.toFixed(3), + stat.createdAt, + ] + reportRows.push(row.map(field => { + if (field === null || field === undefined) return '' + const str = String(field) + if (str.includes(',') || str.includes('"') || str.includes('\n')) { + return `"${str.replace(/"/g, '""')}"` + } + return str + }).join(',')) + } + + const csvContent = reportRows.join('\n') + const outputDir = process.env.IS_IN_CONTAINER === 'true' ? '/tmp' : '.' + const statsFile = `${outputDir}/老素材外网服务器源文件大小统计.csv` + const summaryFile = `${outputDir}/老素材外网服务器源文件大小汇总.csv` + + await writeFile(statsFile, csvContent, 'utf-8') + + console.log(`✅ 报告已生成: ${statsFile}`) + console.log(` 包含 ${sizeStats.length} 个存在文件的大小信息`) + console.log() + + // 生成汇总报告 + const summaryRows: string[] = [] + summaryRows.push('统计项,数值') + summaryRows.push(`总文件数,${sizeStats.length}`) + summaryRows.push(`总大小(字节),${totalSize}`) + summaryRows.push(`总大小(MB),${(totalSize / (1024 * 1024)).toFixed(2)}`) + summaryRows.push(`总大小(GB),${totalSizeGB.toFixed(2)}`) + summaryRows.push(`平均大小(MB),${avgSizeMB.toFixed(2)}`) + summaryRows.push(`最大文件(MB),${(maxSize / (1024 * 1024)).toFixed(2)}`) + summaryRows.push(`最小文件(MB),${(minSize / (1024 * 1024)).toFixed(2)}`) + summaryRows.push('') + summaryRows.push('素材类型,文件数,总大小(GB),平均大小(MB)') + + for (const [type, stats] of Object.entries(typeStats).sort((a, b) => b[1].totalSize - a[1].totalSize)) { + const totalGB = stats.totalSize / (1024 * 1024 * 1024) + const avgMB = stats.avgSize / (1024 * 1024) + summaryRows.push(`${type},${stats.count},${totalGB.toFixed(2)},${avgMB.toFixed(2)}`) + } + + await writeFile(summaryFile, summaryRows.join('\n'), 'utf-8') + + console.log(`✅ 汇总报告已生成: ${summaryFile}`) + console.log() +} + +main().catch((error) => { + console.error('执行失败:', error) + process.exit(1) +})