Skip to main content

dj/
utils.rs

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]; // 2MB of zeros
224        let chunker = Chunker::new();
225        let chunks = chunker.chunk_data(&data);
226
227        assert!(!chunks.is_empty());
228
229        // Verify all chunk data combined equals original
230        let combined: Vec<u8> = chunks.iter().flat_map(|(_, data)| data).cloned().collect();
231        assert_eq!(combined, data);
232
233        // Verify hash consistency
234        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}