1use indicatif::{ProgressBar, ProgressStyle};
5use serde::{Deserialize, Serialize};
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8use tokio::sync::RwLock;
9use tracing::debug;
10
11#[derive(Debug)]
13pub struct ProgressTracker {
14 inner: Arc<RwLock<ProgressTrackerInner>>,
15}
16
17struct ProgressTrackerInner {
18 progress_bar: Option<ProgressBar>,
19 start_time: Instant,
20 total_size: u64,
21 downloaded_size: u64,
22 chunks: Vec<ChunkProgress>,
23 speed_samples: Vec<SpeedSample>,
24 callback: Option<Box<dyn Fn(ProgressInfo) + Send + Sync>>,
25}
26
27impl std::fmt::Debug for ProgressTrackerInner {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 f.debug_struct("ProgressTrackerInner")
30 .field("progress_bar", &self.progress_bar)
31 .field("start_time", &self.start_time)
32 .field("total_size", &self.total_size)
33 .field("downloaded_size", &self.downloaded_size)
34 .field("chunks", &self.chunks)
35 .field("speed_samples", &self.speed_samples)
36 .field("callback", &"<callback>")
37 .finish()
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ProgressInfo {
44 pub total_size: u64,
46
47 pub downloaded_size: u64,
49
50 pub percentage: f64,
52
53 pub speed: f64,
55
56 pub eta: Option<Duration>,
58
59 pub elapsed: Duration,
61
62 pub active_chunks: usize,
64
65 pub complete: bool,
67}
68
69#[derive(Debug, Clone)]
71pub struct ChunkProgress {
72 pub chunk_id: usize,
73 pub start_byte: u64,
74 pub end_byte: u64,
75 pub downloaded: u64,
76 pub active: bool,
77}
78
79#[derive(Debug, Clone)]
81struct SpeedSample {
82 timestamp: Instant,
83 bytes_downloaded: u64,
84}
85
86pub type ProgressCallback = Box<dyn Fn(ProgressInfo) + Send + Sync>;
88
89impl ProgressTracker {
90 pub fn new(total_size: u64) -> Self {
92 let inner = ProgressTrackerInner {
93 progress_bar: None,
94 start_time: Instant::now(),
95 total_size,
96 downloaded_size: 0,
97 chunks: Vec::new(),
98 speed_samples: Vec::new(),
99 callback: None,
100 };
101
102 Self {
103 inner: Arc::new(RwLock::new(inner)),
104 }
105 }
106
107 pub fn with_progress_bar(total_size: u64) -> Self {
109 let progress_bar = ProgressBar::new(total_size);
110 progress_bar.set_style(
111 ProgressStyle::default_bar()
112 .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
113 .unwrap()
114 .progress_chars("#>-"),
115 );
116
117 let inner = ProgressTrackerInner {
118 progress_bar: Some(progress_bar),
119 start_time: Instant::now(),
120 total_size,
121 downloaded_size: 0,
122 chunks: Vec::new(),
123 speed_samples: Vec::new(),
124 callback: None,
125 };
126
127 Self {
128 inner: Arc::new(RwLock::new(inner)),
129 }
130 }
131
132 pub async fn set_callback<F>(&self, callback: F)
134 where
135 F: Fn(ProgressInfo) + Send + Sync + 'static,
136 {
137 let mut inner = self.inner.write().await;
138 inner.callback = Some(Box::new(callback));
139 }
140
141 pub async fn init_chunks(&self, chunk_ranges: Vec<(u64, u64)>) {
143 let mut inner = self.inner.write().await;
144 inner.chunks = chunk_ranges
145 .into_iter()
146 .enumerate()
147 .map(|(id, (start, end))| ChunkProgress {
148 chunk_id: id,
149 start_byte: start,
150 end_byte: end,
151 downloaded: 0,
152 active: false,
153 })
154 .collect();
155 }
156
157 pub async fn update_chunk(&self, chunk_id: usize, bytes_downloaded: u64) {
159 let mut inner = self.inner.write().await;
160
161 if let Some(chunk) = inner.chunks.get_mut(chunk_id) {
162 let old_downloaded = chunk.downloaded;
163 chunk.downloaded = bytes_downloaded;
164 chunk.active = true;
165
166 let delta = bytes_downloaded.saturating_sub(old_downloaded);
168 inner.downloaded_size += delta;
169
170 if let Some(ref pb) = inner.progress_bar {
172 pb.set_position(inner.downloaded_size);
173 }
174
175 let bytes_downloaded = inner.downloaded_size;
177 inner.speed_samples.push(SpeedSample {
178 timestamp: Instant::now(),
179 bytes_downloaded,
180 });
181
182 let cutoff = Instant::now() - Duration::from_secs(10);
184 inner
185 .speed_samples
186 .retain(|sample| sample.timestamp > cutoff);
187
188 if let Some(ref callback) = inner.callback {
190 let progress_info = Self::calculate_progress_info(&inner);
191 callback(progress_info);
192 }
193 }
194 }
195
196 pub async fn complete_chunk(&self, chunk_id: usize) {
198 let mut inner = self.inner.write().await;
199
200 if let Some(chunk) = inner.chunks.get_mut(chunk_id) {
201 chunk.active = false;
202 debug!(
203 "Chunk {} completed: {}/{} bytes",
204 chunk_id,
205 chunk.downloaded,
206 chunk.end_byte - chunk.start_byte + 1
207 );
208 }
209 }
210
211 pub async fn update(&self, bytes_downloaded: u64) {
213 let mut inner = self.inner.write().await;
214 inner.downloaded_size = bytes_downloaded;
215
216 if let Some(ref pb) = inner.progress_bar {
218 pb.set_position(bytes_downloaded);
219 }
220
221 inner.speed_samples.push(SpeedSample {
223 timestamp: Instant::now(),
224 bytes_downloaded,
225 });
226
227 let cutoff = Instant::now() - Duration::from_secs(10);
229 inner
230 .speed_samples
231 .retain(|sample| sample.timestamp > cutoff);
232
233 if let Some(ref callback) = inner.callback {
235 let progress_info = Self::calculate_progress_info(&inner);
236 callback(progress_info);
237 }
238 }
239
240 pub async fn complete(&self) {
242 let mut inner = self.inner.write().await;
243 inner.downloaded_size = inner.total_size;
244
245 if let Some(ref pb) = inner.progress_bar {
246 pb.finish_with_message("Download completed");
247 }
248
249 if let Some(ref callback) = inner.callback {
251 let mut progress_info = Self::calculate_progress_info(&inner);
252 progress_info.complete = true;
253 callback(progress_info);
254 }
255 }
256
257 pub async fn get_progress(&self) -> ProgressInfo {
259 let inner = self.inner.read().await;
260 Self::calculate_progress_info(&inner)
261 }
262
263 pub async fn abort(&self) {
265 let inner = self.inner.read().await;
266 if let Some(ref pb) = inner.progress_bar {
267 pb.abandon_with_message("Download aborted");
268 }
269 }
270
271 fn calculate_progress_info(inner: &ProgressTrackerInner) -> ProgressInfo {
274 let percentage = if inner.total_size > 0 {
275 (inner.downloaded_size as f64 / inner.total_size as f64) * 100.0
276 } else {
277 0.0
278 };
279
280 let elapsed = inner.start_time.elapsed();
281 let speed = Self::calculate_speed(&inner.speed_samples);
282 let eta = Self::calculate_eta(inner.total_size, inner.downloaded_size, speed);
283 let active_chunks = inner.chunks.iter().filter(|c| c.active).count();
284
285 ProgressInfo {
286 total_size: inner.total_size,
287 downloaded_size: inner.downloaded_size,
288 percentage,
289 speed,
290 eta,
291 elapsed,
292 active_chunks,
293 complete: false,
294 }
295 }
296
297 fn calculate_speed(samples: &[SpeedSample]) -> f64 {
298 if samples.len() < 2 {
299 return 0.0;
300 }
301
302 let first = &samples[0];
303 let last = &samples[samples.len() - 1];
304
305 let time_diff = last.timestamp.duration_since(first.timestamp).as_secs_f64();
306 let bytes_diff = last.bytes_downloaded.saturating_sub(first.bytes_downloaded);
307
308 if time_diff > 0.0 {
309 bytes_diff as f64 / time_diff
310 } else {
311 0.0
312 }
313 }
314
315 fn calculate_eta(total_size: u64, downloaded_size: u64, speed: f64) -> Option<Duration> {
316 if speed > 0.0 && downloaded_size < total_size {
317 let remaining_bytes = total_size - downloaded_size;
318 let eta_seconds = remaining_bytes as f64 / speed;
319 Some(Duration::from_secs_f64(eta_seconds))
320 } else {
321 None
322 }
323 }
324}
325
326impl ProgressInfo {
327 pub fn percentage_normalized(&self) -> f64 {
329 self.percentage / 100.0
330 }
331
332 pub fn speed_mbps(&self) -> f64 {
334 self.speed / 1_000_000.0
335 }
336
337 pub fn speed_human(&self) -> String {
339 if self.speed >= 1_000_000_000.0 {
340 format!("{:.2} GB/s", self.speed / 1_000_000_000.0)
341 } else if self.speed >= 1_000_000.0 {
342 format!("{:.2} MB/s", self.speed / 1_000_000.0)
343 } else if self.speed >= 1_000.0 {
344 format!("{:.2} KB/s", self.speed / 1_000.0)
345 } else {
346 format!("{:.0} B/s", self.speed)
347 }
348 }
349
350 pub fn eta_human(&self) -> String {
352 match self.eta {
353 Some(eta) => {
354 let total_seconds = eta.as_secs();
355 let hours = total_seconds / 3600;
356 let minutes = (total_seconds % 3600) / 60;
357 let seconds = total_seconds % 60;
358
359 if hours > 0 {
360 format!("{hours}h {minutes}m {seconds}s")
361 } else if minutes > 0 {
362 format!("{minutes}m {seconds}s")
363 } else {
364 format!("{seconds}s")
365 }
366 }
367 None => "Unknown".to_string(),
368 }
369 }
370
371 pub fn size_human(&self) -> String {
373 Self::format_bytes(self.downloaded_size, self.total_size)
374 }
375
376 fn format_bytes(downloaded: u64, total: u64) -> String {
377 let format_size = |size: u64| -> String {
378 if size >= 1_000_000_000 {
379 format!("{:.2} GB", size as f64 / 1_000_000_000.0)
380 } else if size >= 1_000_000 {
381 format!("{:.2} MB", size as f64 / 1_000_000.0)
382 } else if size >= 1_000 {
383 format!("{:.2} KB", size as f64 / 1_000.0)
384 } else {
385 format!("{size} B")
386 }
387 };
388
389 format!("{} / {}", format_size(downloaded), format_size(total))
390 }
391}
392
393pub struct ConsoleProgressReporter;
395
396impl ConsoleProgressReporter {
397 pub fn default_callback() -> ProgressCallback {
398 Box::new(|progress: ProgressInfo| {
399 println!(
400 "Progress: {:.1}% ({}) - {} - ETA: {}",
401 progress.percentage,
402 progress.size_human(),
403 progress.speed_human(),
404 progress.eta_human()
405 );
406 })
407 }
408}
409
410impl Default for ConsoleProgressReporter {
411 fn default() -> Self {
412 let _tracker = Self::default_callback();
413 Self
414 }
415}