Saber quanto espaço cada diretório ocupa no disco é fundamental para administração de sistemas, limpeza de espaço e diagnóstico de problemas de armazenamento. O comando du (disk usage) do Unix faz exatamente isso, e neste projeto vamos construir uma versão simplificada mas funcional em Rust. Ao percorrer a árvore de diretórios, calcular tamanhos acumulados e apresentar os resultados de forma organizada, você vai aprender sobre o módulo std::fs, a crate walkdir, formatação de dados e ordenação de coleções.
Este projeto combina operações de sistema de arquivos com processamento de dados e formatação — habilidades essenciais para qualquer ferramenta de DevOps ou administração de sistemas escrita em Rust.
O Que Vamos Construir
Nosso espaco terá os seguintes recursos:
- Varredura recursiva de diretórios
- Cálculo acumulado de tamanho por diretório
- Tamanhos legíveis por humanos (KB, MB, GB)
- Ordenação por tamanho (maiores primeiro)
- Limitação do número de resultados exibidos
- Profundidade máxima de varredura configurável
- Barra visual de proporção
Estrutura do Projeto
espaco/
├── Cargo.toml
└── src/
├── main.rs
├── cli.rs
├── scanner.rs
└── formatador.rs
Configurando o Projeto
cargo new espaco
cd espaco
Configure o Cargo.toml:
[package]
name = "espaco"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4", features = ["derive"] }
walkdir = "2"
colored = "2"
A crate walkdir facilita a varredura recursiva de diretórios, tratando erros de permissão e symlinks automaticamente.
Passo 1: Interface de Linha de Comando
// src/cli.rs
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "espaco")]
#[command(about = "Analisador de uso de disco — mostra o tamanho de diretórios")]
pub struct Cli {
/// Diretório a analisar (padrão: diretório atual)
#[arg(default_value = ".")]
pub diretorio: String,
/// Profundidade máxima de varredura
#[arg(short = 'p', long, default_value_t = 3)]
pub profundidade: usize,
/// Número máximo de resultados a exibir
#[arg(short = 'n', long, default_value_t = 20)]
pub limite: usize,
/// Ordenar por tamanho (maiores primeiro)
#[arg(short, long, default_value_t = true)]
pub ordenar: bool,
/// Exibir somente diretórios (ignorar arquivos individuais)
#[arg(short = 'd', long)]
pub somente_diretorios: bool,
/// Tamanho mínimo para exibir (em bytes, ex: 1048576 para 1MB)
#[arg(short = 'm', long, default_value_t = 0)]
pub minimo: u64,
/// Exibir barra visual de proporção
#[arg(short = 'b', long, default_value_t = true)]
pub barra: bool,
}
Passo 2: Scanner de Diretórios
// src/scanner.rs
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
/// Representa um item (arquivo ou diretório) com seu tamanho
#[derive(Debug, Clone)]
pub struct ItemDisco {
pub caminho: PathBuf,
pub tamanho: u64,
pub e_diretorio: bool,
pub profundidade: usize,
}
/// Resultado da varredura completa
pub struct ResultadoVarredura {
pub itens: Vec<ItemDisco>,
pub tamanho_total: u64,
pub total_arquivos: usize,
pub total_diretorios: usize,
pub erros: usize,
}
/// Varre o diretório e calcula tamanhos acumulados
pub fn varrer_diretorio(
caminho_raiz: &str,
profundidade_max: usize,
somente_diretorios: bool,
) -> ResultadoVarredura {
let raiz = Path::new(caminho_raiz);
let mut tamanhos_dirs: HashMap<PathBuf, u64> = HashMap::new();
let mut total_arquivos: usize = 0;
let mut total_diretorios: usize = 0;
let mut erros: usize = 0;
// Primeira passada: coletar tamanhos de todos os arquivos
for entrada in WalkDir::new(raiz)
.into_iter()
{
match entrada {
Ok(e) => {
if e.file_type().is_file() {
total_arquivos += 1;
let tamanho = e.metadata().map(|m| m.len()).unwrap_or(0);
// Acumular tamanho em todos os diretórios ancestrais
let mut ancestral = e.path().parent();
while let Some(dir) = ancestral {
*tamanhos_dirs.entry(dir.to_path_buf()).or_insert(0) += tamanho;
if dir == raiz {
break;
}
ancestral = dir.parent();
}
} else if e.file_type().is_dir() {
total_diretorios += 1;
tamanhos_dirs.entry(e.path().to_path_buf()).or_insert(0);
}
}
Err(_) => {
erros += 1;
}
}
}
let tamanho_total = tamanhos_dirs.get(&raiz.to_path_buf()).copied().unwrap_or(0);
// Construir lista de itens
let profundidade_raiz = raiz.components().count();
let mut itens: Vec<ItemDisco> = tamanhos_dirs
.into_iter()
.filter_map(|(caminho, tamanho)| {
let profundidade_abs = caminho.components().count();
let profundidade_rel = profundidade_abs.saturating_sub(profundidade_raiz);
if profundidade_rel > profundidade_max {
return None;
}
// Pular a própria raiz para evitar duplicata no topo
if caminho == raiz.to_path_buf() && profundidade_rel == 0 {
return None;
}
Some(ItemDisco {
caminho,
tamanho,
e_diretorio: true,
profundidade: profundidade_rel,
})
})
.collect();
// Se não somente diretórios, adicionar também arquivos no nível de profundidade
if !somente_diretorios {
for entrada in WalkDir::new(raiz)
.max_depth(profundidade_max)
.into_iter()
.filter_map(|e| e.ok())
{
if entrada.file_type().is_file() {
let profundidade_abs = entrada.path().components().count();
let profundidade_rel = profundidade_abs.saturating_sub(profundidade_raiz);
if profundidade_rel <= profundidade_max {
let tamanho = entrada.metadata().map(|m| m.len()).unwrap_or(0);
itens.push(ItemDisco {
caminho: entrada.into_path(),
tamanho,
e_diretorio: false,
profundidade: profundidade_rel,
});
}
}
}
}
ResultadoVarredura {
itens,
tamanho_total,
total_arquivos,
total_diretorios,
erros,
}
}
O algoritmo funciona em duas passadas: primeiro percorre todos os arquivos e acumula seus tamanhos nos diretórios ancestrais (usando um HashMap), depois constrói a lista de itens respeitando a profundidade máxima.
Passo 3: Formatação e Exibição
// src/formatador.rs
use crate::scanner::ItemDisco;
use colored::*;
use std::path::Path;
/// Converte bytes para formato legível por humanos
pub fn formatar_tamanho(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
const TB: u64 = 1024 * GB;
if bytes >= TB {
format!("{:.2} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
/// Gera uma barra visual proporcional ao tamanho
fn gerar_barra(proporcao: f64, largura: usize) -> String {
let preenchido = (proporcao * largura as f64).round() as usize;
let vazio = largura.saturating_sub(preenchido);
format!("{}{}", "#".repeat(preenchido), ".".repeat(vazio))
}
/// Exibe os resultados da varredura no terminal
pub fn exibir_resultados(
itens: &[ItemDisco],
tamanho_total: u64,
raiz: &str,
mostrar_barra: bool,
) {
// Cabeçalho
println!(
"\n{} {}\n",
"Uso de disco:".bold().cyan(),
raiz.bold()
);
if itens.is_empty() {
println!("{}", "Nenhum item encontrado.".yellow());
return;
}
// Largura da coluna de tamanho
let largura_tamanho = 10;
let largura_barra = 25;
// Cabeçalho da tabela
if mostrar_barra {
println!(
"{:>largura$} {:>5} {:<barra$} {}",
"Tamanho".bold(),
"%".bold(),
"Proporção".bold(),
"Caminho".bold(),
largura = largura_tamanho,
barra = largura_barra
);
println!("{}", "-".repeat(largura_tamanho + largura_barra + 40));
} else {
println!(
"{:>largura$} {:>5} {}",
"Tamanho".bold(),
"%".bold(),
"Caminho".bold(),
largura = largura_tamanho
);
println!("{}", "-".repeat(largura_tamanho + 40));
}
for item in itens {
let tam = formatar_tamanho(item.tamanho);
let proporcao = if tamanho_total > 0 {
item.tamanho as f64 / tamanho_total as f64
} else {
0.0
};
let percentual = proporcao * 100.0;
let caminho_display = formatar_caminho(&item.caminho, raiz);
// Colorir baseado na proporção
let tam_colorido = if percentual > 50.0 {
tam.red().bold().to_string()
} else if percentual > 20.0 {
tam.yellow().to_string()
} else {
tam.to_string()
};
let icone = if item.e_diretorio { "DIR" } else { " " };
if mostrar_barra {
let barra = gerar_barra(proporcao, largura_barra);
let barra_colorida = if percentual > 50.0 {
barra.red().to_string()
} else if percentual > 20.0 {
barra.yellow().to_string()
} else {
barra.green().to_string()
};
println!(
"{:>largura$} {:>4.1}% {:<barra$} {} {}",
tam_colorido,
percentual,
barra_colorida,
icone.dimmed(),
caminho_display,
largura = largura_tamanho,
barra = largura_barra
);
} else {
println!(
"{:>largura$} {:>4.1}% {} {}",
tam_colorido,
percentual,
icone.dimmed(),
caminho_display,
largura = largura_tamanho
);
}
}
}
/// Formata o caminho relativo à raiz para exibição
fn formatar_caminho(caminho: &Path, raiz: &str) -> String {
let raiz_path = Path::new(raiz);
caminho
.strip_prefix(raiz_path)
.map(|p| p.display().to_string())
.unwrap_or_else(|_| caminho.display().to_string())
}
/// Exibe o resumo da varredura
pub fn exibir_resumo(
tamanho_total: u64,
total_arquivos: usize,
total_diretorios: usize,
erros: usize,
) {
println!("\n{}", "Resumo".bold().cyan());
println!(" Tamanho total: {}", formatar_tamanho(tamanho_total).bold());
println!(" Arquivos: {}", total_arquivos);
println!(" Diretórios: {}", total_diretorios);
if erros > 0 {
println!(" Erros: {} (permissão negada ou inacessível)", erros.to_string().red());
}
}
A função formatar_tamanho converte bytes brutos para unidades legíveis (KB, MB, GB, TB). A barra visual usa caracteres # e . para representar a proporção de cada item em relação ao total.
Passo 4: Integrando no main.rs
// src/main.rs
mod cli;
mod formatador;
mod scanner;
use clap::Parser;
use cli::Cli;
use std::path::Path;
fn main() {
let cli = Cli::parse();
// Verificar se o diretório existe
let caminho = Path::new(&cli.diretorio);
if !caminho.exists() {
eprintln!(
"Erro: '{}' não existe.",
cli.diretorio
);
std::process::exit(1);
}
if !caminho.is_dir() {
eprintln!(
"Erro: '{}' não é um diretório.",
cli.diretorio
);
std::process::exit(1);
}
// Executar a varredura
let resultado = scanner::varrer_diretorio(
&cli.diretorio,
cli.profundidade,
cli.somente_diretorios,
);
// Filtrar e ordenar itens
let mut itens = resultado.itens;
// Aplicar filtro de tamanho mínimo
if cli.minimo > 0 {
itens.retain(|item| item.tamanho >= cli.minimo);
}
// Ordenar por tamanho (maiores primeiro)
if cli.ordenar {
itens.sort_by(|a, b| b.tamanho.cmp(&a.tamanho));
}
// Limitar número de resultados
itens.truncate(cli.limite);
// Exibir resultados
formatador::exibir_resultados(
&itens,
resultado.tamanho_total,
&cli.diretorio,
cli.barra,
);
formatador::exibir_resumo(
resultado.tamanho_total,
resultado.total_arquivos,
resultado.total_diretorios,
resultado.erros,
);
}
Como Executar
cargo build --release
Exemplos de uso:
# Analisar o diretório atual
./target/release/espaco
# Analisar um diretório específico
./target/release/espaco /home/usuario/projetos
# Limitar profundidade e quantidade de resultados
./target/release/espaco /var/log -p 2 -n 10
# Exibir somente diretórios
./target/release/espaco ~/Documentos -d
# Filtrar itens menores que 1MB
./target/release/espaco /home -m 1048576
# Saída sem barra visual
./target/release/espaco . --barra false
# Exemplo de saída:
#
# Uso de disco: /home/usuario/projetos
#
# Tamanho % Proporção Caminho
# ------------------------------------------------------------------
# 245.30 MB 45.2% ###########.............. DIR target/
# 89.50 MB 16.5% ####..................... DIR node_modules/
# 34.20 MB 6.3% ##....................... DIR .git/
# 12.80 MB 2.4% #........................ DIR src/
# 5.60 MB 1.0% ......................... DIR docs/
#
# Resumo
# Tamanho total: 542.10 MB
# Arquivos: 12453
# Diretórios: 876
Desafios para Expandir
Modo interativo com navegação: Use a crate
crosstermoutuipara criar uma interface interativa que permita navegar entre diretórios, expandir/recolher subpastas e deletar itens.Detecção de arquivos duplicados: Calcule hashes (SHA-256) dos arquivos para encontrar duplicatas e mostrar quanto espaço poderia ser economizado removendo-as.
Exportação para JSON/CSV: Adicione uma flag
--formato jsonou--formato csvque exporte os resultados em formato estruturado para integração com outras ferramentas.Cache de varredura: Salve o resultado da varredura em um cache local e na próxima execução, compare com o estado anterior para mostrar o que cresceu ou diminuiu desde a última análise.
Filtro por tipo de arquivo: Adicione opções como
--tipo imagensou--extensao mp4para calcular o espaço ocupado por categorias específicas de arquivos.
Veja Também
- Módulo Path — trabalhando com caminhos de arquivos e diretórios
- Módulo fs — operações de sistema de arquivos em Rust
- HashMap — acumulação de dados por chave
- Lendo Arquivos — padrões de leitura de arquivos
- Rust para CLIs — boas práticas para ferramentas de linha de comando