Skip to content

Instantly share code, notes, and snippets.

@malei0311
Created June 5, 2025 13:14
Show Gist options
  • Select an option

  • Save malei0311/911acd40e1643b3d8d9eee08ba933d21 to your computer and use it in GitHub Desktop.

Select an option

Save malei0311/911acd40e1643b3d8d9eee08ba933d21 to your computer and use it in GitHub Desktop.

Revisions

  1. malei0311 renamed this gist Jun 5, 2025. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. malei0311 created this gist Jun 5, 2025.
    339 changes: 339 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,339 @@
    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;