Skip to content

Commit 2956833

Browse files
committed
feat: improve save/load command output formatting
- Match the styled output format used by 'info' command - Show MFT structure details (records, utilization, fragmentation) - Display absolute file paths - Show compression ratio and space saved - Add timing information - Use box-drawing characters and emoji for visual clarity
1 parent ccd7e12 commit 2956833

File tree

1 file changed

+148
-23
lines changed

1 file changed

+148
-23
lines changed

crates/uffs-mft/src/main.rs

Lines changed: 148 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1888,10 +1888,52 @@ async fn cmd_save(
18881888
compress: bool,
18891889
compression_level: i32,
18901890
) -> Result<()> {
1891+
use std::time::Instant;
1892+
1893+
use uffs_mft::platform::{VolumeHandle, detect_drive_type};
18911894
use uffs_mft::{MftReader, SaveRawOptions};
18921895

1893-
info!(drive = %drive, "Reading raw MFT from drive");
1896+
let start_time = Instant::now();
1897+
let drive_upper = drive.to_ascii_uppercase();
1898+
1899+
info!(drive = %drive_upper, "Reading raw MFT from drive");
18941900

1901+
// Get volume info for display
1902+
let handle =
1903+
VolumeHandle::open(drive).with_context(|| format!("Failed to open {}:", drive))?;
1904+
let vol_data = handle.volume_data();
1905+
1906+
let drive_type = detect_drive_type(drive_upper);
1907+
let drive_type_str = match drive_type {
1908+
uffs_mft::DriveType::Ssd => "SSD",
1909+
uffs_mft::DriveType::Hdd => "HDD",
1910+
uffs_mft::DriveType::Unknown => "Unknown",
1911+
};
1912+
1913+
// Calculate metrics
1914+
let record_count =
1915+
vol_data.mft_valid_data_length / vol_data.bytes_per_file_record_segment as u64;
1916+
1917+
// Fragmentation analysis
1918+
let mut extent_count = 1;
1919+
let is_fragmented;
1920+
if let Ok(extents) = handle.get_mft_extents() {
1921+
extent_count = extents.len();
1922+
is_fragmented = extent_count > 1;
1923+
} else {
1924+
is_fragmented = false;
1925+
}
1926+
1927+
// Bitmap analysis
1928+
let mut in_use_records = 0u64;
1929+
let mut utilization = 0.0f64;
1930+
if let Ok(bitmap) = handle.get_mft_bitmap() {
1931+
in_use_records = bitmap.count_in_use() as u64;
1932+
utilization = (in_use_records as f64 / record_count as f64) * 100.0;
1933+
}
1934+
let free_records = record_count.saturating_sub(in_use_records);
1935+
1936+
// Open reader and save
18951937
let reader = MftReader::open(drive)
18961938
.await
18971939
.with_context(|| format!("Failed to open drive {drive}:"))?;
@@ -1906,20 +1948,62 @@ async fn cmd_save(
19061948
.await
19071949
.with_context(|| format!("Failed to save raw MFT to {}", output.display()))?;
19081950

1951+
let elapsed = start_time.elapsed();
1952+
1953+
// Get absolute path for display
1954+
let abs_path = std::fs::canonicalize(output).unwrap_or_else(|_| output.to_path_buf());
1955+
1956+
// Print formatted output
1957+
println!("═══════════════════════════════════════════════════════════════");
1958+
println!(" MFT SAVED");
1959+
println!(
1960+
" Drive: {}: ({})",
1961+
drive_upper, drive_type_str
1962+
);
1963+
println!("═══════════════════════════════════════════════════════════════");
19091964
println!();
1910-
println!("=== Raw MFT Saved ===");
1911-
println!("Output: {}", output.display());
1912-
println!("Records: {}", header.record_count);
1913-
println!("Record size: {} bytes", header.record_size);
1914-
println!("Original size: {}", format_bytes(header.original_size));
1965+
println!("📁 MFT STRUCTURE");
1966+
println!(
1967+
" Total records: {}",
1968+
format_number_commas(record_count)
1969+
);
1970+
println!(
1971+
" In-use records: {}",
1972+
format_number_commas(in_use_records)
1973+
);
1974+
println!(
1975+
" Free records: {}",
1976+
format_number_commas(free_records)
1977+
);
1978+
println!(" Utilization: {:.1}%", utilization);
1979+
println!(
1980+
" Fragmentation: {} extent(s) {}",
1981+
extent_count,
1982+
if is_fragmented { "⚠️" } else { "✅" }
1983+
);
1984+
println!();
1985+
println!("💾 OUTPUT FILE");
1986+
println!(" Path: {}", abs_path.display());
1987+
println!(
1988+
" Original size: {}",
1989+
format_bytes(header.original_size)
1990+
);
19151991
if header.is_compressed() {
1916-
println!("Compressed size: {}", format_bytes(header.compressed_size));
1992+
println!(
1993+
" Compressed size: {}",
1994+
format_bytes(header.compressed_size)
1995+
);
19171996
#[allow(clippy::cast_precision_loss, clippy::float_arithmetic)]
19181997
let ratio = header.compressed_size as f64 / header.original_size as f64 * 100.0_f64;
1919-
println!("Compression: {ratio:.1}%");
1998+
println!(" Compression ratio: {ratio:.1}%");
1999+
#[allow(clippy::cast_precision_loss, clippy::float_arithmetic)]
2000+
let savings = 100.0_f64 - ratio;
2001+
println!(" Space saved: {savings:.1}%");
19202002
} else {
1921-
println!("Compression: none");
2003+
println!(" Compression: none");
19222004
}
2005+
println!();
2006+
println!("⏱️ Completed in {}", format_duration(elapsed));
19232007

19242008
Ok(())
19252009
}
@@ -1939,66 +2023,107 @@ async fn cmd_save(
19392023
/// Load MFT from a saved file and optionally export.
19402024
#[cfg(windows)]
19412025
async fn cmd_load(input: &Path, output: Option<&Path>, info_only: bool) -> Result<()> {
2026+
use std::time::Instant;
2027+
19422028
use uffs_mft::{MftReader, load_raw_mft_header};
19432029

2030+
let start_time = Instant::now();
2031+
19442032
// Load header first
19452033
let header = load_raw_mft_header(input)
19462034
.with_context(|| format!("Failed to load raw MFT header from {}", input.display()))?;
19472035

1948-
println!("=== Raw MFT File Info ===");
1949-
println!("File: {}", input.display());
1950-
println!("Version: {}", header.version);
1951-
println!("Records: {}", header.record_count);
1952-
println!("Record size: {} bytes", header.record_size);
1953-
println!("Original size: {}", format_bytes(header.original_size));
2036+
// Get absolute path for display
2037+
let abs_path = std::fs::canonicalize(input).unwrap_or_else(|_| input.to_path_buf());
2038+
2039+
// Print formatted output
2040+
println!("═══════════════════════════════════════════════════════════════");
2041+
println!(" MFT FILE INFO");
2042+
println!("═══════════════════════════════════════════════════════════════");
2043+
println!();
2044+
println!("📁 FILE DETAILS");
2045+
println!(" Path: {}", abs_path.display());
2046+
println!(" Format version: {}", header.version);
2047+
println!();
2048+
println!("📊 MFT CONTENTS");
2049+
println!(
2050+
" Total records: {}",
2051+
format_number_commas(header.record_count.into())
2052+
);
2053+
println!(
2054+
" Record size: {} bytes",
2055+
format_number_commas(header.record_size.into())
2056+
);
2057+
println!(
2058+
" Original size: {}",
2059+
format_bytes(header.original_size)
2060+
);
19542061
if header.is_compressed() {
1955-
println!("Compressed size: {}", format_bytes(header.compressed_size));
2062+
println!(
2063+
" Compressed size: {}",
2064+
format_bytes(header.compressed_size)
2065+
);
19562066
#[allow(clippy::cast_precision_loss, clippy::float_arithmetic)]
19572067
let ratio = header.compressed_size as f64 / header.original_size as f64 * 100.0_f64;
1958-
println!("Compression: {ratio:.1}%");
2068+
println!(" Compression ratio: {ratio:.1}%");
19592069
} else {
1960-
println!("Compression: none");
2070+
println!(" Compression: none");
19612071
}
19622072

19632073
if info_only {
2074+
println!();
2075+
let elapsed = start_time.elapsed();
2076+
println!("⏱️ Completed in {}", format_duration(elapsed));
19642077
return Ok(());
19652078
}
19662079

19672080
// Parse and export
19682081
let output = output.context("--output is required when not using --info-only")?;
19692082

1970-
info!("Parsing MFT records");
2083+
println!();
2084+
println!("📤 EXPORTING...");
19712085

19722086
let df = MftReader::load_raw_to_dataframe(input)
19732087
.with_context(|| format!("Failed to parse raw MFT from {}", input.display()))?;
19742088

1975-
info!(records = df.height(), "Parsed records");
2089+
let parsed_count = df.height();
19762090

19772091
// Determine output format from extension
19782092
let ext = output
19792093
.extension()
19802094
.and_then(|e| e.to_str())
19812095
.unwrap_or("parquet");
19822096

2097+
let output_abs = std::fs::canonicalize(output).unwrap_or_else(|_| output.to_path_buf());
2098+
19832099
match ext {
19842100
"csv" => {
19852101
use std::fs::File;
19862102
use std::io::Write;
19872103

19882104
let mut file = File::create(output)?;
1989-
// Simple CSV export
19902105
let mut df = df;
19912106
let csv_str = uffs_polars::write_csv_to_string(&mut df)?;
19922107
file.write_all(csv_str.as_bytes())?;
1993-
info!(path = %output.display(), "Exported to CSV");
2108+
println!(" Format: CSV");
19942109
}
19952110
_ => {
19962111
let mut df = df;
19972112
MftReader::save_parquet(&mut df, output)?;
1998-
info!(path = %output.display(), "Exported to Parquet");
2113+
println!(" Format: Parquet");
19992114
}
20002115
}
20012116

2117+
println!(" Output path: {}", output_abs.display());
2118+
println!(
2119+
" Records exported: {}",
2120+
format_number_commas(parsed_count as u64)
2121+
);
2122+
2123+
let elapsed = start_time.elapsed();
2124+
println!();
2125+
println!("⏱️ Completed in {}", format_duration(elapsed));
2126+
20022127
Ok(())
20032128
}
20042129

0 commit comments

Comments
 (0)