1use crate::{data::HashId, Result, CHUNK_MAX_SIZE, CHUNK_MIN_SIZE, CHUNK_NORMAL_SIZE};
2use fastcdc::v2020::FastCDC;
3use std::io::Read;
4use tokio::io::{AsyncRead, AsyncReadExt};
5
6#[derive(Debug, Clone)]
7pub struct Chunker {
8 min_size: usize,
9 max_size: usize,
10 normal_size: usize,
11}
12
13impl Chunker {
14 pub fn new() -> Self {
15 Self {
16 min_size: CHUNK_MIN_SIZE,
17 max_size: CHUNK_MAX_SIZE,
18 normal_size: CHUNK_NORMAL_SIZE,
19 }
20 }
21
22 pub fn with_sizes(min_size: usize, normal_size: usize, max_size: usize) -> Self {
23 Self {
24 min_size,
25 max_size,
26 normal_size,
27 }
28 }
29
30 pub fn chunk_data(&self, data: &[u8]) -> Vec<(HashId, Vec<u8>)> {
31 if data.is_empty() {
32 return Vec::new();
33 }
34
35 let mut chunks = Vec::new();
36 let chunker = FastCDC::new(
37 data,
38 self.min_size as u32,
39 self.normal_size as u32,
40 self.max_size as u32,
41 );
42
43 for chunk in chunker {
44 let chunk_data = data[chunk.offset..chunk.offset + chunk.length].to_vec();
45 let hash = HashId::new(&chunk_data);
46 chunks.push((hash, chunk_data));
47 }
48
49 chunks
50 }
51
52 pub async fn chunk_async_reader<R>(&self, mut reader: R) -> Result<Vec<(HashId, Vec<u8>)>>
53 where
54 R: AsyncRead + Unpin,
55 {
56 let mut buffer = Vec::new();
57 reader.read_to_end(&mut buffer).await?;
58 Ok(self.chunk_data(&buffer))
59 }
60
61 pub fn chunk_reader<R>(&self, mut reader: R) -> Result<Vec<(HashId, Vec<u8>)>>
62 where
63 R: Read,
64 {
65 let mut buffer = Vec::new();
66 reader.read_to_end(&mut buffer)?;
67 Ok(self.chunk_data(&buffer))
68 }
69}
70
71impl Default for Chunker {
72 fn default() -> Self {
73 Self::new()
74 }
75}
76
77pub fn calculate_hash(data: &[u8]) -> HashId {
78 HashId::new(data)
79}
80
81pub fn format_bytes(bytes: u64) -> String {
82 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB"];
83 let mut size = bytes as f64;
84 let mut unit_index = 0;
85
86 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
87 size /= 1024.0;
88 unit_index += 1;
89 }
90
91 if unit_index == 0 {
92 format!("{} {}", bytes, UNITS[unit_index])
93 } else {
94 format!("{:.2} {}", size, UNITS[unit_index])
95 }
96}
97
98pub fn format_duration(duration: std::time::Duration) -> String {
99 let total_seconds = duration.as_secs();
100
101 if total_seconds < 60 {
102 format!("{}s", total_seconds)
103 } else if total_seconds < 3600 {
104 let minutes = total_seconds / 60;
105 let seconds = total_seconds % 60;
106 format!("{}m{}s", minutes, seconds)
107 } else {
108 let hours = total_seconds / 3600;
109 let minutes = (total_seconds % 3600) / 60;
110 let seconds = total_seconds % 60;
111 format!("{}h{}m{}s", hours, minutes, seconds)
112 }
113}
114
115pub fn estimate_eta(
116 processed: u64,
117 total: u64,
118 elapsed: std::time::Duration,
119) -> Option<std::time::Duration> {
120 if processed == 0 || elapsed.is_zero() {
121 return None;
122 }
123
124 let rate = processed as f64 / elapsed.as_secs_f64();
125 if rate <= 0.0 {
126 return None;
127 }
128
129 let remaining = total.saturating_sub(processed);
130 let eta_seconds = (remaining as f64 / rate) as u64;
131 Some(std::time::Duration::from_secs(eta_seconds))
132}
133
134pub fn calculate_transfer_rate(bytes: u64, duration: std::time::Duration) -> f64 {
135 if duration.is_zero() {
136 return 0.0;
137 }
138 bytes as f64 / duration.as_secs_f64()
139}
140
141pub fn format_transfer_rate(rate: f64) -> String {
142 format_bytes(rate as u64) + "/s"
143}
144
145pub fn sanitize_filename(filename: &str) -> String {
146 filename
147 .chars()
148 .map(|c| match c {
149 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
150 c if c.is_control() => '_',
151 c => c,
152 })
153 .collect()
154}
155
156pub fn truncate_string(s: &str, max_len: usize) -> String {
157 if s.len() <= max_len {
158 s.to_string()
159 } else {
160 format!("{}...", &s[..max_len.saturating_sub(3)])
161 }
162}
163
164#[derive(Debug, Clone)]
165pub struct ProgressTracker {
166 pub processed_files: u64,
167 pub total_files: u64,
168 pub processed_bytes: u64,
169 pub total_bytes: u64,
170 pub current_file: Option<String>,
171 pub start_time: std::time::Instant,
172}
173
174impl ProgressTracker {
175 pub fn new(total_files: u64, total_bytes: u64) -> Self {
176 Self {
177 processed_files: 0,
178 total_files,
179 processed_bytes: 0,
180 total_bytes,
181 current_file: None,
182 start_time: std::time::Instant::now(),
183 }
184 }
185
186 pub fn update_file(&mut self, filename: String, file_size: u64) {
187 self.current_file = Some(filename);
188 self.processed_files += 1;
189 self.processed_bytes += file_size;
190 }
191
192 pub fn update_bytes(&mut self, bytes: u64) {
193 self.processed_bytes += bytes;
194 }
195
196 pub fn elapsed(&self) -> std::time::Duration {
197 self.start_time.elapsed()
198 }
199
200 pub fn eta(&self) -> Option<std::time::Duration> {
201 estimate_eta(self.processed_bytes, self.total_bytes, self.elapsed())
202 }
203
204 pub fn transfer_rate(&self) -> f64 {
205 calculate_transfer_rate(self.processed_bytes, self.elapsed())
206 }
207
208 pub fn progress_ratio(&self) -> f64 {
209 if self.total_bytes == 0 {
210 0.0
211 } else {
212 self.processed_bytes as f64 / self.total_bytes as f64
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_chunker() {
223 let data = vec![0u8; 2 * 1024 * 1024]; let chunker = Chunker::new();
225 let chunks = chunker.chunk_data(&data);
226
227 assert!(!chunks.is_empty());
228
229 let combined: Vec<u8> = chunks.iter().flat_map(|(_, data)| data).cloned().collect();
231 assert_eq!(combined, data);
232
233 for (hash, chunk_data) in &chunks {
235 assert_eq!(*hash, HashId::new(chunk_data));
236 }
237 }
238
239 #[test]
240 fn test_format_bytes() {
241 assert_eq!(format_bytes(512), "512 B");
242 assert_eq!(format_bytes(1024), "1.00 KB");
243 assert_eq!(format_bytes(1536), "1.50 KB");
244 assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
245 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
246 }
247
248 #[test]
249 fn test_format_duration() {
250 assert_eq!(format_duration(std::time::Duration::from_secs(30)), "30s");
251 assert_eq!(format_duration(std::time::Duration::from_secs(90)), "1m30s");
252 assert_eq!(
253 format_duration(std::time::Duration::from_secs(3661)),
254 "1h1m1s"
255 );
256 }
257
258 #[test]
259 fn test_sanitize_filename() {
260 assert_eq!(sanitize_filename("normal_file.txt"), "normal_file.txt");
261 assert_eq!(
262 sanitize_filename("file/with\\bad:chars"),
263 "file_with_bad_chars"
264 );
265 assert_eq!(sanitize_filename("file<>|?*.txt"), "file_____.txt");
266 }
267
268 #[test]
269 fn test_progress_tracker() {
270 let mut tracker = ProgressTracker::new(10, 1000);
271
272 assert_eq!(tracker.processed_files, 0);
273 assert_eq!(tracker.processed_bytes, 0);
274 assert_eq!(tracker.progress_ratio(), 0.0);
275
276 tracker.update_file("test.txt".to_string(), 100);
277 assert_eq!(tracker.processed_files, 1);
278 assert_eq!(tracker.processed_bytes, 100);
279 assert_eq!(tracker.progress_ratio(), 0.1);
280
281 tracker.update_bytes(400);
282 assert_eq!(tracker.processed_bytes, 500);
283 assert_eq!(tracker.progress_ratio(), 0.5);
284 }
285}