Skip to content

Commit e540eba

Browse files
committed
feat: enhance uffs info command with rich statistics
- Show file/directory counts, size metrics, attributes - Display multi-stream and multi-name file counts - Match the rich formatting style of uffs_mft commands - Fix sum() method usage for polars ChunkedArray - Fix CSV export to use polars CsvWriter directly
1 parent d2421bd commit e540eba

File tree

2 files changed

+115
-12
lines changed

2 files changed

+115
-12
lines changed

crates/uffs-cli/src/commands.rs

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -599,20 +599,123 @@ pub fn info(path: &Path) -> Result<()> {
599599
let df = MftReader::load_parquet(path)
600600
.with_context(|| format!("Failed to load index: {}", path.display()))?;
601601

602-
let mut stdout = std::io::stdout().lock();
603-
writeln!(stdout, "Index: {}", path.display())?;
604-
writeln!(stdout, "Records: {}", df.height())?;
605-
writeln!(stdout, "Columns: {}", df.width())?;
606-
writeln!(stdout)?;
607-
writeln!(stdout, "Schema:")?;
602+
// Get absolute path and file size
603+
let abs_path = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
604+
let file_size = std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
605+
606+
let total_records = df.height();
607+
608+
// Extract statistics from the DataFrame
609+
let dir_count = df
610+
.column("is_directory")
611+
.ok()
612+
.and_then(|c| c.bool().ok())
613+
.map(|b| b.sum().unwrap_or(0) as u64)
614+
.unwrap_or(0);
615+
let file_count = (total_records as u64).saturating_sub(dir_count);
616+
617+
// Helper closure to count bool columns
618+
let count_bool = |name: &str| -> u64 {
619+
df.column(name)
620+
.ok()
621+
.and_then(|c| c.bool().ok())
622+
.map(|b| b.sum().unwrap_or(0) as u64)
623+
.unwrap_or(0)
624+
};
625+
626+
let hidden_count = count_bool("is_hidden");
627+
let system_count = count_bool("is_system");
628+
let compressed_count = count_bool("is_compressed");
629+
let encrypted_count = count_bool("is_encrypted");
630+
let sparse_count = count_bool("is_sparse");
631+
let reparse_count = count_bool("is_reparse");
632+
let readonly_count = count_bool("is_readonly");
633+
let archive_count = count_bool("is_archive");
634+
635+
// Total size calculation
636+
let total_size: u64 = df
637+
.column("size")
638+
.ok()
639+
.and_then(|c| c.u64().ok())
640+
.map(|s| s.iter().flatten().sum())
641+
.unwrap_or(0);
642+
643+
// Allocated size calculation
644+
let total_allocated: u64 = df
645+
.column("allocated_size")
646+
.ok()
647+
.and_then(|c| c.u64().ok())
648+
.map(|s| s.iter().flatten().sum())
649+
.unwrap_or(0);
650+
651+
// Count multi-stream and multi-name files
652+
let multi_stream_count = df
653+
.column("stream_count")
654+
.ok()
655+
.and_then(|c| c.u16().ok())
656+
.map(|s| s.iter().filter(|v| v.is_some_and(|x| x > 1)).count() as u64)
657+
.unwrap_or(0);
658+
let multi_name_count = df
659+
.column("name_count")
660+
.ok()
661+
.and_then(|c| c.u16().ok())
662+
.map(|s| s.iter().filter(|v| v.is_some_and(|x| x > 1)).count() as u64)
663+
.unwrap_or(0);
664+
665+
println!("═══════════════════════════════════════════════════════════════");
666+
println!(" INDEX FILE INFO");
667+
println!("═══════════════════════════════════════════════════════════════");
668+
println!();
669+
println!("📁 FILE DETAILS");
670+
println!(" Path: {}", abs_path.display());
671+
println!(" File size: {}", format_size(file_size));
672+
println!(" Columns: {}", df.width());
673+
println!();
674+
println!("📊 RECORD STATISTICS");
675+
println!(" Total records: {}", format_number(total_records as u64));
676+
println!(" Directories: {}", format_number(dir_count));
677+
println!(" Files: {}", format_number(file_count));
678+
println!();
679+
println!("💾 SIZE METRICS");
680+
println!(" Total file size: {}", format_size(total_size));
681+
println!(" Total allocated: {}", format_size(total_allocated));
682+
println!();
683+
println!("🏷️ ATTRIBUTES");
684+
println!(" Hidden: {}", format_number(hidden_count));
685+
println!(" System: {}", format_number(system_count));
686+
println!(" Read-only: {}", format_number(readonly_count));
687+
println!(" Archive: {}", format_number(archive_count));
688+
println!(" Compressed: {}", format_number(compressed_count));
689+
println!(" Encrypted: {}", format_number(encrypted_count));
690+
println!(" Sparse: {}", format_number(sparse_count));
691+
println!(" Reparse points: {}", format_number(reparse_count));
692+
println!();
693+
println!("🔗 ADVANCED");
694+
println!(" Multi-stream files: {}", format_number(multi_stream_count));
695+
println!(" Multi-name files: {}", format_number(multi_name_count));
696+
println!();
697+
println!("📋 SCHEMA");
608698
let schema = df.schema();
609699
for (name, dtype) in schema.iter() {
610-
writeln!(stdout, " {name}: {dtype}")?;
700+
println!(" {name}: {dtype}");
611701
}
612702

613703
Ok(())
614704
}
615705

706+
/// Format a number with comma separators.
707+
fn format_number(n: u64) -> String {
708+
let s = n.to_string();
709+
let mut result = String::with_capacity(s.len() + s.len() / 3);
710+
for (i, c) in s.chars().rev().enumerate() {
711+
if i > 0 && i % 3 == 0 {
712+
result.push(',');
713+
}
714+
result.push(c);
715+
}
716+
result.chars().rev().collect()
717+
}
718+
616719
/// Format file size in human-readable format.
617720
#[allow(clippy::cast_precision_loss, clippy::float_arithmetic)]
618721
fn format_size(bytes: u64) -> String {

crates/uffs-mft/src/main.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,7 +2116,7 @@ async fn cmd_load(input: &Path, output: Option<&Path>, info_only: bool) -> Resul
21162116
.column("size")
21172117
.ok()
21182118
.and_then(|c| c.u64().ok())
2119-
.map(|s| s.sum().unwrap_or(0))
2119+
.map(|s| s.iter().flatten().sum())
21202120
.unwrap_or(0);
21212121

21222122
println!();
@@ -2178,13 +2178,13 @@ async fn cmd_load(input: &Path, output: Option<&Path>, info_only: bool) -> Resul
21782178

21792179
match ext {
21802180
"csv" => {
2181+
use polars::prelude::CsvWriter;
2182+
use polars::prelude::SerWriter;
21812183
use std::fs::File;
2182-
use std::io::Write;
21832184

2184-
let mut file = File::create(output)?;
2185+
let file = File::create(output)?;
21852186
let mut df = df;
2186-
let csv_str = uffs_polars::write_csv_to_string(&mut df)?;
2187-
file.write_all(csv_str.as_bytes())?;
2187+
CsvWriter::new(file).finish(&mut df)?;
21882188
}
21892189
_ => {
21902190
let mut df = df;

0 commit comments

Comments
 (0)