@@ -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) ]
19412025async 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