forked from
atscan.net/plcbundle-rs
High-performance implementation of plcbundle written in Rust
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}