High-performance implementation of plcbundle written in Rust
at main 261 lines 8.9 kB view raw
1use anyhow::{Context, Result}; 2use clap::{Args, ValueHint}; 3use plcbundle::{BundleManager, constants, remote::RemoteClient}; 4use std::path::PathBuf; 5use std::sync::Arc; 6 7#[derive(Args)] 8#[command( 9 about = "Clone a remote bundle repository", 10 long_about = "Download all bundles from a remote plcbundle HTTP server to create a complete 11local copy of the repository. Similar to 'git clone', this command creates a new 12repository directory and populates it with all bundles from the remote source. 13 14Bundles are downloaded in parallel for maximum speed, and the plc_bundles.json index 15file is automatically reconstructed during the process. The command checks available 16disk space before starting and warns if there's insufficient space. 17 18Use --resume to continue a partial clone that was interrupted, skipping bundles that 19already exist. The command validates bundle integrity during download to ensure data 20correctness. 21 22This is the fastest way to create a local copy of an existing repository, whether 23for backup, local development, or creating a mirror. After cloning, the repository 24is immediately ready to use with all standard commands.", 25 help_template = crate::clap_help!( 26 examples: " # Clone from remote instance\n \ 27 {bin} clone https://plc.example.com /path/to/local\n\n \ 28 # Clone to current directory\n \ 29 {bin} clone https://plc.example.com .\n\n \ 30 # Clone with custom parallelism\n \ 31 {bin} clone https://plc.example.com /path/to/local --parallel 8" 32 ) 33)] 34pub struct CloneCommand { 35 /// Remote plcbundle instance URL 36 #[arg(value_hint = ValueHint::Url)] 37 pub source_url: String, 38 39 /// Target directory for cloned repository 40 #[arg(value_hint = ValueHint::DirPath)] 41 pub target_dir: PathBuf, 42 43 /// Number of parallel downloads (default: 4) 44 #[arg(long, default_value = "4")] 45 pub parallel: usize, 46 47 /// Resume partial clone (skip existing bundles) 48 #[arg(long)] 49 pub resume: bool, 50} 51 52pub fn run(cmd: CloneCommand) -> Result<()> { 53 // Create tokio runtime for async operations 54 tokio::runtime::Runtime::new()?.block_on(async { run_async(cmd).await }) 55} 56 57async fn run_async(cmd: CloneCommand) -> Result<()> { 58 use super::utils::display_path; 59 60 // Validate parallel count 61 if cmd.parallel == 0 || cmd.parallel > 32 { 62 anyhow::bail!("Parallel downloads must be between 1 and 32"); 63 } 64 65 // Resolve target directory to absolute path 66 let target_dir = if cmd.target_dir.is_absolute() { 67 cmd.target_dir.clone() 68 } else { 69 std::env::current_dir()?.join(&cmd.target_dir) 70 }; 71 72 println!("Cloning from: {}", cmd.source_url); 73 println!("Target: {}", display_path(&target_dir).display()); 74 println!("Parallelism: {} downloads", cmd.parallel); 75 println!(); 76 77 // Create remote client 78 let client = RemoteClient::new(&cmd.source_url)?; 79 80 // Fetch remote index 81 println!("Fetching index..."); 82 let remote_index = client 83 .fetch_index() 84 .await 85 .context("Failed to fetch remote index")?; 86 87 let last_bundle = remote_index.last_bundle; 88 89 // Get root hash (first bundle) and head hash (last bundle) 90 let root_hash = remote_index 91 .bundles 92 .first() 93 .map(|b| b.hash.as_str()) 94 .unwrap_or("(none)"); 95 let head_hash = remote_index 96 .bundles 97 .last() 98 .map(|b| b.hash.as_str()) 99 .unwrap_or("(none)"); 100 101 // Format total compressed size 102 let total_size = remote_index.total_size_bytes; 103 let size_display = plcbundle::format::format_bytes(total_size); 104 105 println!("✓ Remote index fetched"); 106 println!(" Version: {}", remote_index.version); 107 println!(" Origin: {}", remote_index.origin); 108 println!(" Last bundle: {}", last_bundle); 109 println!(" Total size: {}", size_display); 110 println!(" Root hash: {}", root_hash); 111 println!(" Head hash: {}", head_hash); 112 println!(); 113 114 // Create target directory if it doesn't exist 115 if !target_dir.exists() { 116 std::fs::create_dir_all(&target_dir).context("Failed to create target directory")?; 117 } 118 119 // Check if target directory is empty or if resuming 120 let index_path = target_dir.join("plc_bundles.json"); 121 if index_path.exists() && !cmd.resume { 122 anyhow::bail!( 123 "Target directory already contains plc_bundles.json\nUse --resume to continue partial clone" 124 ); 125 } 126 127 // Determine which bundles to download 128 let bundles_to_download: Vec<u32> = if cmd.resume { 129 // Check which bundles already exist 130 let existing_count = remote_index 131 .bundles 132 .iter() 133 .filter(|meta| { 134 let bundle_path = constants::bundle_path(&target_dir, meta.bundle_number); 135 bundle_path.exists() 136 }) 137 .count(); 138 139 if existing_count > 0 { 140 println!("Resuming: {} bundles already downloaded", existing_count); 141 } 142 143 remote_index 144 .bundles 145 .iter() 146 .filter_map(|meta| { 147 let bundle_path = constants::bundle_path(&target_dir, meta.bundle_number); 148 if !bundle_path.exists() { 149 Some(meta.bundle_number) 150 } else { 151 None 152 } 153 }) 154 .collect() 155 } else { 156 remote_index 157 .bundles 158 .iter() 159 .map(|meta| meta.bundle_number) 160 .collect() 161 }; 162 163 let bundles_count = bundles_to_download.len(); 164 if bundles_count == 0 { 165 println!("✓ All bundles already downloaded"); 166 return Ok(()); 167 } 168 169 // Calculate total bytes to download 170 let total_bytes: u64 = remote_index 171 .bundles 172 .iter() 173 .filter(|meta| bundles_to_download.contains(&meta.bundle_number)) 174 .map(|meta| meta.compressed_size) 175 .sum(); 176 177 // Check available disk space and warn if insufficient 178 if let Some(free_space) = super::utils::get_free_disk_space(&target_dir) { 179 // Add 10% buffer for safety (filesystem overhead, temporary files, etc.) 180 let required_space = total_bytes + (total_bytes / 10); 181 182 if free_space < required_space { 183 let free_display = plcbundle::format::format_bytes(free_space); 184 let required_display = plcbundle::format::format_bytes(required_space); 185 let shortfall = required_space - free_space; 186 let shortfall_display = plcbundle::format::format_bytes(shortfall); 187 188 eprintln!("⚠️ Warning: Insufficient disk space"); 189 eprintln!(" Required: {}", required_display); 190 eprintln!(" Available: {}", free_display); 191 eprintln!(" Shortfall: {}", shortfall_display); 192 eprintln!(); 193 194 // Prompt user to continue 195 use dialoguer::Confirm; 196 let proceed = Confirm::new() 197 .with_prompt("Do you want to continue anyway? (This may fail partway through)") 198 .default(false) 199 .interact() 200 .context("Failed to read user input")?; 201 202 if !proceed { 203 anyhow::bail!("Clone cancelled by user"); 204 } 205 206 println!(); 207 } 208 } 209 210 println!("Downloading {} bundle(s)...", bundles_count); 211 println!(); 212 213 // Create progress bar with byte tracking 214 let progress = Arc::new(super::progress::ProgressBar::with_bytes( 215 bundles_count, 216 total_bytes, 217 )); 218 219 // Clone using BundleManager API with progress callback 220 let progress_clone = Arc::clone(&progress); 221 let (downloaded_count, failed_count) = BundleManager::clone_from_remote( 222 cmd.source_url.clone(), 223 &target_dir, 224 &remote_index, 225 bundles_to_download, 226 Some(move |_bundle_num, count, _total, bytes| { 227 progress_clone.set_with_bytes(count, bytes); 228 }), 229 ) 230 .await?; 231 232 progress.finish(); 233 println!(); 234 235 if failed_count > 0 { 236 eprintln!( 237 "✗ Clone incomplete: {} succeeded, {} failed", 238 downloaded_count, failed_count 239 ); 240 eprintln!(" Use --resume to retry failed downloads"); 241 anyhow::bail!("Clone failed"); 242 } 243 244 println!("✓ Clone complete!"); 245 println!(" Location: {}", display_path(&target_dir).display()); 246 println!(" Bundles: {}", downloaded_count); 247 println!(); 248 println!("Next steps:"); 249 println!(" cd {}", display_path(&target_dir).display()); 250 println!( 251 " {} status # Check repository status", 252 constants::BINARY_NAME 253 ); 254 println!( 255 " {} sync # Sync to latest", 256 constants::BINARY_NAME 257 ); 258 println!(" {} server --sync # Run server", constants::BINARY_NAME); 259 260 Ok(()) 261}