1use std::sync::Arc;
4
5#[derive(Debug, Clone)]
7pub struct DownloadProgress {
8 pub total_bytes: Option<u64>,
10
11 pub downloaded_bytes: u64,
13
14 pub speed_bps: u64,
16
17 pub eta_seconds: Option<u64>,
19
20 pub filename: String,
22}
23
24impl DownloadProgress {
25 pub fn new(filename: impl Into<String>, total_bytes: Option<u64>) -> Self {
27 Self {
28 total_bytes,
29 downloaded_bytes: 0,
30 speed_bps: 0,
31 eta_seconds: None,
32 filename: filename.into(),
33 }
34 }
35
36 pub fn fraction(&self) -> Option<f64> {
38 self.total_bytes.map(|total| {
39 if total == 0 {
40 1.0
41 } else {
42 self.downloaded_bytes as f64 / total as f64
43 }
44 })
45 }
46
47 pub fn percent(&self) -> Option<u8> {
49 self.fraction().map(|f| (f * 100.0).min(100.0) as u8)
50 }
51
52 pub fn speed_string(&self) -> String {
54 format_bytes_per_second(self.speed_bps)
55 }
56
57 pub fn size_string(&self) -> String {
59 match self.total_bytes {
60 Some(total) => format!(
61 "{} / {}",
62 format_bytes(self.downloaded_bytes),
63 format_bytes(total)
64 ),
65 None => format_bytes(self.downloaded_bytes),
66 }
67 }
68}
69
70pub type ProgressCallback = Arc<dyn Fn(&DownloadProgress) + Send + Sync>;
72
73pub fn no_progress() -> ProgressCallback {
75 Arc::new(|_| {})
76}
77
78fn format_bytes(bytes: u64) -> String {
80 const KB: u64 = 1024;
81 const MB: u64 = KB * 1024;
82 const GB: u64 = MB * 1024;
83
84 if bytes >= GB {
85 format!("{:.2} GB", bytes as f64 / GB as f64)
86 } else if bytes >= MB {
87 format!("{:.2} MB", bytes as f64 / MB as f64)
88 } else if bytes >= KB {
89 format!("{:.1} KB", bytes as f64 / KB as f64)
90 } else {
91 format!("{} B", bytes)
92 }
93}
94
95fn format_bytes_per_second(bps: u64) -> String {
97 format!("{}/s", format_bytes(bps))
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn test_progress_fraction() {
106 let mut progress = DownloadProgress::new("test.zip", Some(1000));
107 progress.downloaded_bytes = 500;
108
109 assert_eq!(progress.fraction(), Some(0.5));
110 assert_eq!(progress.percent(), Some(50));
111 }
112
113 #[test]
114 fn test_progress_unknown_total() {
115 let progress = DownloadProgress::new("test.zip", None);
116 assert_eq!(progress.fraction(), None);
117 assert_eq!(progress.percent(), None);
118 }
119
120 #[test]
121 fn test_format_bytes() {
122 assert_eq!(format_bytes(500), "500 B");
123 assert_eq!(format_bytes(1024), "1.0 KB");
124 assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
125 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
126 }
127
128 #[test]
129 fn test_size_string() {
130 let mut progress = DownloadProgress::new("test.zip", Some(1024 * 1024));
131 progress.downloaded_bytes = 512 * 1024;
132
133 assert_eq!(progress.size_string(), "512.0 KB / 1.00 MB");
134 }
135
136 #[test]
137 fn test_size_string_unknown_total() {
138 let mut progress = DownloadProgress::new("test.zip", None);
139 progress.downloaded_bytes = 2048;
140 assert_eq!(progress.size_string(), "2.0 KB");
141 }
142
143 #[test]
144 fn test_fraction_zero_total_is_complete() {
145 let progress = DownloadProgress::new("empty.zip", Some(0));
147 assert_eq!(progress.fraction(), Some(1.0));
148 assert_eq!(progress.percent(), Some(100));
149 }
150
151 #[test]
152 fn test_percent_clamped_to_100() {
153 let mut progress = DownloadProgress::new("test.zip", Some(1000));
155 progress.downloaded_bytes = 1500;
156 assert_eq!(progress.percent(), Some(100));
157 }
158
159 #[test]
160 fn test_speed_string() {
161 let mut progress = DownloadProgress::new("test.zip", None);
162 progress.speed_bps = 1024 * 1024;
163 assert_eq!(progress.speed_string(), "1.00 MB/s");
164
165 progress.speed_bps = 500;
166 assert_eq!(progress.speed_string(), "500 B/s");
167 }
168
169 #[test]
170 fn test_format_bytes_per_second() {
171 assert_eq!(format_bytes_per_second(0), "0 B/s");
172 assert_eq!(format_bytes_per_second(1024), "1.0 KB/s");
173 assert_eq!(format_bytes_per_second(1024 * 1024 * 1024), "1.00 GB/s");
174 }
175
176 #[test]
177 fn test_new_initializes_fields() {
178 let progress = DownloadProgress::new("file.zip", Some(99));
179 assert_eq!(progress.filename, "file.zip");
180 assert_eq!(progress.total_bytes, Some(99));
181 assert_eq!(progress.downloaded_bytes, 0);
182 assert_eq!(progress.speed_bps, 0);
183 assert_eq!(progress.eta_seconds, None);
184 }
185}