#!/usr/bin/env node /** * scan.js - 目录扫描工具 * 扫描指定目录,生成文件清单和分析报告 */ const fs = require('fs-extra'); const path = require('path'); const { program } = require('commander'); // 文件类型定义 const FILE_TYPES = { APPLICATION: 'application', DOCUMENT: 'document', IMAGE: 'image', VIDEO: 'video', AUDIO: 'audio', ARCHIVE: 'archive', CODE: 'code', OTHER: 'other' }; // 扩展名映射 const EXTENSION_MAP = { // 应用程序 '.app': FILE_TYPES.APPLICATION, '.dmg': FILE_TYPES.APPLICATION, '.pkg': FILE_TYPES.APPLICATION, '.exe': FILE_TYPES.APPLICATION, // 文档 '.pdf': FILE_TYPES.DOCUMENT, '.doc': FILE_TYPES.DOCUMENT, '.docx': FILE_TYPES.DOCUMENT, '.xls': FILE_TYPES.DOCUMENT, '.xlsx': FILE_TYPES.DOCUMENT, '.ppt': FILE_TYPES.DOCUMENT, '.pptx': FILE_TYPES.DOCUMENT, '.txt': FILE_TYPES.DOCUMENT, '.md': FILE_TYPES.DOCUMENT, '.rtf': FILE_TYPES.DOCUMENT, // 图片 '.jpg': FILE_TYPES.IMAGE, '.jpeg': FILE_TYPES.IMAGE, '.png': FILE_TYPES.IMAGE, '.gif': FILE_TYPES.IMAGE, '.bmp': FILE_TYPES.IMAGE, '.svg': FILE_TYPES.IMAGE, '.webp': FILE_TYPES.IMAGE, '.heic': FILE_TYPES.IMAGE, // 视频 '.mp4': FILE_TYPES.VIDEO, '.mov': FILE_TYPES.VIDEO, '.avi': FILE_TYPES.VIDEO, '.mkv': FILE_TYPES.VIDEO, '.wmv': FILE_TYPES.VIDEO, '.flv': FILE_TYPES.VIDEO, '.webm': FILE_TYPES.VIDEO, // 音频 '.mp3': FILE_TYPES.AUDIO, '.wav': FILE_TYPES.AUDIO, '.flac': FILE_TYPES.AUDIO, '.aac': FILE_TYPES.AUDIO, '.m4a': FILE_TYPES.AUDIO, '.ogg': FILE_TYPES.AUDIO, // 压缩包 '.zip': FILE_TYPES.ARCHIVE, '.rar': FILE_TYPES.ARCHIVE, '.7z': FILE_TYPES.ARCHIVE, '.tar': FILE_TYPES.ARCHIVE, '.gz': FILE_TYPES.ARCHIVE, '.bz2': FILE_TYPES.ARCHIVE, // 代码 '.js': FILE_TYPES.CODE, '.ts': FILE_TYPES.CODE, '.jsx': FILE_TYPES.CODE, '.tsx': FILE_TYPES.CODE, '.py': FILE_TYPES.CODE, '.java': FILE_TYPES.CODE, '.c': FILE_TYPES.CODE, '.cpp': FILE_TYPES.CODE, '.h': FILE_TYPES.CODE, '.hpp': FILE_TYPES.CODE, '.css': FILE_TYPES.CODE, '.scss': FILE_TYPES.CODE, '.html': FILE_TYPES.CODE, '.json': FILE_TYPES.CODE, '.xml': FILE_TYPES.CODE, '.yaml': FILE_TYPES.CODE, '.yml': FILE_TYPES.CODE, }; /** * 提取版本号 * 支持多种版本号格式: * - v1.2.3 * - 1.2.3 * - V1.2.3 * - version 1.2.3 */ function extractVersion(filename) { const patterns = [ /v(\d+\.\d+\.\d+)/i, /version[_\s-]?(\d+\.\d+\.\d+)/i, /(\d+\.\d+\.\d+)/, /v(\d+\.\d+)/i, /(\d+\.\d+)/, ]; for (const pattern of patterns) { const match = filename.match(pattern); if (match) { return match[1]; } } return null; } /** * 获取文件类型 */ function getFileType(filename) { const ext = path.extname(filename).toLowerCase(); return EXTENSION_MAP[ext] || FILE_TYPES.OTHER; } /** * 获取文件信息 */ async function getFileInfo(filePath) { const stats = await fs.stat(filePath); const filename = path.basename(filePath); const ext = path.extname(filename); const basename = path.basename(filename, ext); return { path: filePath, filename, basename, extension: ext, type: getFileType(filename), size: stats.size, created: stats.birthtime, modified: stats.mtime, isDirectory: stats.isDirectory(), version: extractVersion(filename), }; } /** * 扫描目录 */ async function scanDirectory(targetPath) { console.log(`扫描目录: ${targetPath}`); if (!await fs.pathExists(targetPath)) { throw new Error(`目录不存在: ${targetPath}`); } const files = []; const items = await fs.readdir(targetPath); for (const item of items) { // 跳过隐藏文件(以 . 开头的文件) if (item.startsWith('.')) { continue; } const itemPath = path.join(targetPath, item); try { const fileInfo = await getFileInfo(itemPath); files.push(fileInfo); } catch (error) { console.warn(`警告: 无法读取文件信息 ${itemPath}:`, error.message); } } return files; } /** * 分析扫描结果 */ function analyzeFiles(files) { const analysis = { total: files.length, byType: {}, byExtension: {}, totalSize: 0, duplicates: [], versions: {}, }; // 按类型分组 for (const file of files) { if (!analysis.byType[file.type]) { analysis.byType[file.type] = []; } analysis.byType[file.type].push(file); // 按扩展名分组 if (file.extension) { if (!analysis.byExtension[file.extension]) { analysis.byExtension[file.extension] = 0; } analysis.byExtension[file.extension]++; } // 累计大小 analysis.totalSize += file.size; // 识别可能的版本 if (file.version) { const baseName = file.basename.replace(/[v_\s-]?\d+\.\d+(\.\d+)?/gi, '').trim(); if (!analysis.versions[baseName]) { analysis.versions[baseName] = []; } analysis.versions[baseName].push({ filename: file.filename, version: file.version, path: file.path, }); } } return analysis; } /** * 格式化文件大小 */ function formatSize(bytes) { const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)} ${units[unitIndex]}`; } /** * 生成报告 */ function generateReport(targetPath, files, analysis) { console.log('\n=== 扫描报告 ===\n'); console.log(`目录: ${targetPath}`); console.log(`总文件数: ${analysis.total}`); console.log(`总大小: ${formatSize(analysis.totalSize)}\n`); console.log('文件类型分布:'); for (const [type, typeFiles] of Object.entries(analysis.byType)) { console.log(` ${type}: ${typeFiles.length} 个文件`); } console.log('\n扩展名分布:'); const sortedExtensions = Object.entries(analysis.byExtension) .sort((a, b) => b[1] - a[1]) .slice(0, 10); for (const [ext, count] of sortedExtensions) { console.log(` ${ext}: ${count} 个文件`); } console.log('\n检测到的版本:'); for (const [baseName, versions] of Object.entries(analysis.versions)) { if (versions.length > 1) { console.log(` ${baseName}:`); for (const v of versions) { console.log(` - ${v.filename} (v${v.version})`); } } } } /** * 主函数 */ async function main() { program .name('scan') .description('扫描目录并生成文件清单') .argument('', '要扫描的目录路径') .option('-o, --output ', '输出 JSON 文件路径') .option('-v, --verbose', '显示详细信息') .parse(); const targetPath = path.resolve(program.args[0]); const options = program.opts(); try { const files = await scanDirectory(targetPath); const analysis = analyzeFiles(files); generateReport(targetPath, files, analysis); // 如果指定了输出文件,保存 JSON if (options.output) { const outputPath = path.resolve(options.output); const data = { scannedAt: new Date().toISOString(), targetPath, files, analysis, }; await fs.writeJson(outputPath, data, { spaces: 2 }); console.log(`\n报告已保存到: ${outputPath}`); } // 返回结果供其他脚本使用 return { files, analysis }; } catch (error) { console.error('错误:', error.message); process.exit(1); } } // 如果直接运行此脚本 if (require.main === module) { main(); } // 导出供其他模块使用 module.exports = { scanDirectory, analyzeFiles, getFileType, extractVersion, };