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.
du.js
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