Created
June 5, 2025 13:14
-
-
Save malei0311/911acd40e1643b3d8d9eee08ba933d21 to your computer and use it in GitHub Desktop.
du.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const fs = require('fs').promises; | |
| const path = require('path'); | |
| class NodeDU { | |
| constructor(options = {}) { | |
| this.maxDepth = options.maxDepth || Infinity; | |
| this.humanReadable = options.humanReadable !== false; // 默认为 true | |
| this.summarize = options.summarize || false; | |
| this.allFiles = options.allFiles || false; | |
| this.excludeDirs = options.excludeDirs || []; | |
| this.sortBySize = options.sortBySize || false; | |
| this.showProgress = options.showProgress || false; | |
| } | |
| async getFileSize(filePath) { | |
| try { | |
| const stats = await fs.stat(filePath); | |
| return stats.size; | |
| } catch (error) { | |
| if (this.showProgress) { | |
| console.error(`Warning: Cannot access ${filePath}: ${error.message}`); | |
| } | |
| return 0; | |
| } | |
| } | |
| async calculateDirectorySize(directoryPath, currentDepth = 0, basePath = null) { | |
| const results = []; | |
| const absolutePath = path.resolve(directoryPath); | |
| if (basePath === null) { | |
| basePath = absolutePath; | |
| } | |
| // 检查是否应该排除此目录 | |
| const dirName = path.basename(absolutePath); | |
| if (this.excludeDirs.includes(dirName)) { | |
| return { totalSize: 0, results: [] }; | |
| } | |
| let totalDirectorySize = 0; | |
| const subdirectories = []; | |
| const files = []; | |
| try { | |
| const entries = await fs.readdir(absolutePath, { withFileTypes: true }); | |
| // 分别处理文件和目录 | |
| for (const entry of entries) { | |
| const entryPath = path.join(absolutePath, entry.name); | |
| if (entry.isFile()) { | |
| const fileSize = await this.getFileSize(entryPath); | |
| totalDirectorySize += fileSize; | |
| if (this.allFiles) { | |
| files.push({ | |
| path: entryPath, | |
| size: fileSize, | |
| isFile: true, | |
| depth: currentDepth | |
| }); | |
| } | |
| } else if (entry.isDirectory()) { | |
| subdirectories.push(entryPath); | |
| } | |
| } | |
| // 处理子目录 | |
| for (const subDir of subdirectories) { | |
| const subDirName = path.basename(subDir); | |
| // 检查排除列表 | |
| if (this.excludeDirs.includes(subDirName)) { | |
| continue; | |
| } | |
| const subResult = await this.calculateDirectorySize(subDir, currentDepth + 1, basePath); | |
| totalDirectorySize += subResult.totalSize; | |
| // 根据深度限制决定是否包含子目录结果 | |
| if (currentDepth + 1 <= this.maxDepth) { | |
| results.push({ | |
| path: subDir, | |
| size: subResult.totalSize, | |
| isFile: false, | |
| depth: currentDepth + 1 | |
| }); | |
| // 如果不是汇总模式且深度允许,添加子结果 | |
| if (!this.summarize && currentDepth + 1 < this.maxDepth) { | |
| results.push(...subResult.results); | |
| } | |
| } | |
| } | |
| // 添加文件结果 | |
| if (this.allFiles) { | |
| results.push(...files); | |
| } | |
| } catch (error) { | |
| if (this.showProgress) { | |
| console.error(`Warning: Cannot read directory ${absolutePath}: ${error.message}`); | |
| } | |
| } | |
| return { totalSize: totalDirectorySize, results }; | |
| } | |
| formatBytes(bytes, decimals = 1) { | |
| if (!this.humanReadable) { | |
| return bytes.toString(); | |
| } | |
| if (bytes === 0) return '0B'; | |
| const k = 1024; | |
| const dm = decimals < 0 ? 0 : decimals; | |
| const sizes = ['B', 'K', 'M', 'G', 'T', 'P']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| const value = bytes / Math.pow(k, i); | |
| const formattedValue = i === 0 ? value.toString() : value.toFixed(dm); | |
| return `${formattedValue}${sizes[i]}`; | |
| } | |
| getRelativePath(fullPath, basePath) { | |
| const relative = path.relative(basePath, fullPath); | |
| return relative || '.'; | |
| } | |
| async analyze(targetPath) { | |
| const startTime = Date.now(); | |
| try { | |
| const stats = await fs.stat(targetPath); | |
| if (!stats.isDirectory()) { | |
| // 如果是文件,直接返回文件大小 | |
| const size = await this.getFileSize(targetPath); | |
| return { | |
| totalSize: size, | |
| results: [{ | |
| path: targetPath, | |
| size: size, | |
| isFile: true, | |
| depth: 0 | |
| }] | |
| }; | |
| } | |
| } catch (error) { | |
| throw new Error(`Cannot access ${targetPath}: ${error.message}`); | |
| } | |
| if (this.showProgress) { | |
| console.log(`Analyzing directory: ${targetPath}`); | |
| } | |
| const result = await this.calculateDirectorySize(targetPath); | |
| if (this.showProgress) { | |
| const duration = ((Date.now() - startTime) / 1000).toFixed(2); | |
| console.log(`Analysis completed in ${duration}s\n`); | |
| } | |
| return result; | |
| } | |
| printResults(results, basePath) { | |
| let output = []; | |
| if (this.summarize) { | |
| // 汇总模式:只显示顶级目录的总大小 | |
| const totalSize = results.totalSize; | |
| const relativePath = this.getRelativePath(basePath, process.cwd()); | |
| output.push(`${this.formatBytes(totalSize)}\t${relativePath}`); | |
| } else { | |
| // 详细模式:显示所有符合条件的目录和文件 | |
| let items = results.results.filter(item => item.depth <= this.maxDepth); | |
| if (this.sortBySize) { | |
| items.sort((a, b) => b.size - a.size); | |
| } | |
| items.forEach(item => { | |
| const relativePath = this.getRelativePath(item.path, basePath); | |
| const sizeStr = this.formatBytes(item.size); | |
| const indent = ' '.repeat(item.depth); | |
| const type = item.isFile ? ' (file)' : ''; | |
| output.push(`${sizeStr}\t${indent}${relativePath}${type}`); | |
| }); | |
| // 添加总计(根目录) | |
| const totalStr = this.formatBytes(results.totalSize); | |
| const rootPath = this.getRelativePath(basePath, process.cwd()); | |
| output.push(`${totalStr}\t${rootPath} (total)`); | |
| } | |
| return output.join('\n'); | |
| } | |
| } | |
| // 命令行接口 | |
| function parseArguments() { | |
| const args = process.argv.slice(2); | |
| const options = { | |
| humanReadable: true, | |
| summarize: false, | |
| allFiles: false, | |
| excludeDirs: [], | |
| sortBySize: false, | |
| showProgress: false, | |
| maxDepth: Infinity | |
| }; | |
| let targetPath = '.'; // 默认当前目录 | |
| for (let i = 0; i < args.length; i++) { | |
| const arg = args[i]; | |
| switch (arg) { | |
| case '-h': | |
| case '--human-readable': | |
| options.humanReadable = true; | |
| break; | |
| case '-b': | |
| case '--bytes': | |
| options.humanReadable = false; | |
| break; | |
| case '-s': | |
| case '--summarize': | |
| options.summarize = true; | |
| break; | |
| case '-a': | |
| case '--all': | |
| options.allFiles = true; | |
| break; | |
| case '--sort': | |
| options.sortBySize = true; | |
| break; | |
| case '--progress': | |
| options.showProgress = true; | |
| break; | |
| case '-d': | |
| case '--max-depth': | |
| if (i + 1 < args.length) { | |
| const depth = parseInt(args[i + 1], 10); | |
| if (!isNaN(depth) && depth >= 0) { | |
| options.maxDepth = depth; | |
| i++; // 跳过下一个参数 | |
| } else { | |
| console.error('Error: Invalid depth value'); | |
| process.exit(1); | |
| } | |
| } else { | |
| console.error('Error: --max-depth requires a numeric argument'); | |
| process.exit(1); | |
| } | |
| break; | |
| case '--exclude': | |
| if (i + 1 < args.length) { | |
| options.excludeDirs.push(args[i + 1]); | |
| i++; // 跳过下一个参数 | |
| } else { | |
| console.error('Error: --exclude requires a directory name'); | |
| process.exit(1); | |
| } | |
| break; | |
| case '--help': | |
| showHelp(); | |
| process.exit(0); | |
| break; | |
| default: | |
| if (!arg.startsWith('-')) { | |
| targetPath = arg; | |
| } else { | |
| console.error(`Error: Unknown option ${arg}`); | |
| process.exit(1); | |
| } | |
| break; | |
| } | |
| } | |
| return { options, targetPath }; | |
| } | |
| function showHelp() { | |
| console.log(` | |
| Node.js Directory Usage Analyzer (du-like tool) | |
| Usage: node du.js [OPTIONS] [DIRECTORY] | |
| Options: | |
| -h, --human-readable Display sizes in human readable format (default) | |
| -b, --bytes Display sizes in bytes | |
| -s, --summarize Display only total size for each directory | |
| -a, --all Include individual files in output | |
| -d, --max-depth NUM Limit directory traversal to NUM levels deep | |
| --exclude DIR Exclude directories with name DIR | |
| --sort Sort output by size (largest first) | |
| --progress Show progress during analysis | |
| --help Show this help message | |
| Examples: | |
| node du.js # Analyze current directory | |
| node du.js ~/Downloads # Analyze Downloads folder | |
| node du.js -s -d 1 ~/Projects # Summarize first-level subdirs in Projects | |
| node du.js -a --sort ~/Documents # Show all files, sorted by size | |
| node du.js --exclude node_modules . # Exclude node_modules directories | |
| node du.js -d 2 --progress ~/ # Analyze home directory, 2 levels deep, with progress | |
| Note: Similar to Unix 'du' command but implemented in Node.js | |
| `); | |
| } | |
| // 主函数 | |
| async function main() { | |
| try { | |
| const { options, targetPath } = parseArguments(); | |
| const analyzer = new NodeDU(options); | |
| const results = await analyzer.analyze(targetPath); | |
| const output = analyzer.printResults(results, path.resolve(targetPath)); | |
| console.log(output); | |
| } catch (error) { | |
| console.error(`Error: ${error.message}`); | |
| process.exit(1); | |
| } | |
| } | |
| // 运行程序 | |
| if (require.main === module) { | |
| main(); | |
| } | |
| module.exports = NodeDU; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment