High-performance implementation of plcbundle written in Rust
at main 181 lines 5.5 kB view raw
1use super::utils; 2use anyhow::Result; 3use clap::Args; 4use plcbundle::CleanPreview; 5use std::io::{self, Write}; 6use std::path::PathBuf; 7 8#[derive(Args)] 9#[command( 10 about = "Remove all temporary files from the repository", 11 alias = "cleanup", 12 long_about = "Remove temporary files created during atomic write operations. These files 13have .tmp extensions and are used to ensure data integrity when writing index 14files, DID index shards, and other critical repository data. 15 16Temporary files should normally be cleaned up automatically when operations 17complete successfully. However, if a process is interrupted (e.g., by Ctrl+C 18or a crash), temporary files may remain on disk. This command safely removes 19all such files after showing you what will be deleted. 20 21This is a safe operation that only affects temporary files. Your actual bundle 22data and index files are never touched. Use this command periodically or after 23interrupted operations to keep your repository clean.", 24 help_template = crate::clap_help!( 25 examples: " # Clean all temporary files (with confirmation)\n \ 26 {bin} clean\n\n \ 27 # Clean without confirmation prompt\n \ 28 {bin} clean --force\n\n \ 29 # Clean with verbose output\n \ 30 {bin} clean --verbose\n\n \ 31 # Using alias\n \ 32 {bin} cleanup" 33 ) 34)] 35pub struct CleanCommand { 36 /// Show verbose output 37 #[arg(short, long)] 38 pub verbose: bool, 39 40 /// Skip confirmation prompt 41 #[arg(short, long)] 42 pub force: bool, 43} 44 45pub fn run(cmd: CleanCommand, dir: PathBuf, global_verbose: bool) -> Result<()> { 46 let verbose = cmd.verbose || global_verbose; 47 let manager = utils::create_manager(dir.clone(), verbose, false, false)?; 48 49 // Step 1: Preview what will be cleaned 50 let preview = manager.clean_preview()?; 51 52 // Step 2: Display preview 53 display_clean_preview(&dir, &preview, verbose)?; 54 55 // Step 3: Ask for confirmation (unless --force) 56 if preview.files.is_empty() { 57 println!("✓ No temporary files found - repository is clean"); 58 return Ok(()); 59 } 60 61 if !cmd.force { 62 if !confirm_clean()? { 63 println!("Cancelled"); 64 return Ok(()); 65 } 66 println!(); 67 } 68 69 // Step 4: Perform cleanup 70 if verbose { 71 println!("Cleaning temporary files from repository..."); 72 println!("Directory: {}\n", utils::display_path(&dir).display()); 73 } 74 75 let result = manager.clean()?; 76 77 // Step 5: Display results 78 println!("✓ Cleaned {} temporary file(s)", result.files_removed); 79 if result.bytes_freed > 0 { 80 println!(" Freed: {}", utils::format_bytes(result.bytes_freed)); 81 } 82 83 // Show errors if any 84 if let Some(errors) = &result.errors 85 && !errors.is_empty() 86 { 87 eprintln!("\n⚠ Warning: Some errors occurred during cleanup:"); 88 for error in errors { 89 eprintln!(" - {}", error); 90 } 91 } 92 93 Ok(()) 94} 95 96fn display_clean_preview(dir: &PathBuf, preview: &CleanPreview, _verbose: bool) -> Result<()> { 97 println!("Files to be deleted:"); 98 println!(); 99 100 if preview.files.is_empty() { 101 return Ok(()); 102 } 103 104 // Group files by directory for better display 105 let mut root_files = Vec::new(); 106 let mut config_files = Vec::new(); 107 let mut shard_files = Vec::new(); 108 109 for file in &preview.files { 110 let path_str = file.path.to_string_lossy(); 111 if path_str.contains(".plcbundle/shards/") { 112 shard_files.push(file); 113 } else if path_str.contains(".plcbundle/") { 114 config_files.push(file); 115 } else { 116 root_files.push(file); 117 } 118 } 119 120 // Display root files 121 if !root_files.is_empty() { 122 println!(" Repository root:"); 123 for file in &root_files { 124 let rel_path = file 125 .path 126 .strip_prefix(dir) 127 .unwrap_or(&file.path) 128 .to_string_lossy(); 129 println!("{} ({})", rel_path, utils::format_bytes(file.size)); 130 } 131 println!(); 132 } 133 134 // Display config files 135 if !config_files.is_empty() { 136 println!(" DID index directory:"); 137 for file in &config_files { 138 let rel_path = file 139 .path 140 .strip_prefix(dir) 141 .unwrap_or(&file.path) 142 .to_string_lossy(); 143 println!("{} ({})", rel_path, utils::format_bytes(file.size)); 144 } 145 println!(); 146 } 147 148 // Display shard files 149 if !shard_files.is_empty() { 150 println!(" Shards directory:"); 151 for file in &shard_files { 152 let rel_path = file 153 .path 154 .strip_prefix(dir) 155 .unwrap_or(&file.path) 156 .to_string_lossy(); 157 println!("{} ({})", rel_path, utils::format_bytes(file.size)); 158 } 159 println!(); 160 } 161 162 println!( 163 " Total: {} file(s), {}", 164 preview.files.len(), 165 utils::format_bytes(preview.total_size) 166 ); 167 println!(); 168 169 Ok(()) 170} 171 172fn confirm_clean() -> Result<bool> { 173 print!("Delete these files? [y/N]: "); 174 io::stdout().flush()?; 175 176 let mut response = String::new(); 177 io::stdin().read_line(&mut response)?; 178 179 let response = response.trim().to_lowercase(); 180 Ok(response == "y" || response == "yes") 181}