exarch_core/creation/
progress.rs

1//! Progress tracking utilities for archive creation.
2//!
3//! This module provides reusable progress tracking components that work
4//! with the `ProgressCallback` trait. These utilities consolidate progress
5//! reporting logic to avoid duplication across TAR and ZIP creation.
6//!
7//! # Components
8//!
9//! - **`ProgressTracker`**: Manages progress callbacks with automatic entry
10//!   counting
11//! - **`ProgressReader`**: Wrapper reader that reports bytes read to progress
12//!   callback
13
14use crate::ProgressCallback;
15use std::io::Read;
16use std::path::Path;
17
18/// Manages progress callbacks with automatic entry counting.
19///
20/// This struct consolidates progress-related state to reduce argument count
21/// in helper functions. It automatically tracks the current entry number
22/// and provides convenient methods for common progress reporting patterns.
23///
24/// # Batching
25///
26/// Progress callbacks are invoked at specific lifecycle points:
27///
28/// - `on_entry_start`: Called before processing each entry
29/// - `on_entry_complete`: Called after successfully processing an entry
30/// - `on_complete`: Called once when the entire operation finishes
31///
32/// # Examples
33///
34/// ```
35/// use exarch_core::ProgressCallback;
36/// use exarch_core::creation::progress::ProgressTracker;
37/// use std::path::Path;
38///
39/// struct SimpleProgress;
40///
41/// impl ProgressCallback for SimpleProgress {
42///     fn on_entry_start(&mut self, path: &Path, total: usize, current: usize) {
43///         println!("[{}/{}] Processing: {}", current, total, path.display());
44///     }
45///
46///     fn on_bytes_written(&mut self, bytes: u64) {}
47///
48///     fn on_entry_complete(&mut self, path: &Path) {}
49///
50///     fn on_complete(&mut self) {}
51/// }
52///
53/// let mut progress = SimpleProgress;
54/// let total_entries = 10;
55/// let mut tracker = ProgressTracker::new(&mut progress, total_entries);
56///
57/// tracker.on_entry_start(Path::new("file1.txt"));
58/// // ... process entry ...
59/// tracker.on_entry_complete(Path::new("file1.txt"));
60/// ```
61pub struct ProgressTracker<'a> {
62    /// Reference to the progress callback implementation
63    progress: &'a mut dyn ProgressCallback,
64    /// Current entry number (1-indexed for user display)
65    current_entry: usize,
66    /// Total number of entries to process
67    total_entries: usize,
68}
69
70impl<'a> ProgressTracker<'a> {
71    /// Creates a new progress tracker.
72    ///
73    /// # Parameters
74    ///
75    /// - `progress`: Mutable reference to the progress callback
76    /// - `total_entries`: Total number of entries that will be processed
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// use exarch_core::ProgressCallback;
82    /// use exarch_core::creation::progress::ProgressTracker;
83    /// use std::path::Path;
84    ///
85    /// # struct DummyProgress;
86    /// # impl ProgressCallback for DummyProgress {
87    /// #     fn on_entry_start(&mut self, _: &Path, _: usize, _: usize) {}
88    /// #     fn on_bytes_written(&mut self, _: u64) {}
89    /// #     fn on_entry_complete(&mut self, _: &Path) {}
90    /// #     fn on_complete(&mut self) {}
91    /// # }
92    /// let mut progress = DummyProgress;
93    /// let tracker = ProgressTracker::new(&mut progress, 100);
94    /// ```
95    #[must_use]
96    pub fn new(progress: &'a mut dyn ProgressCallback, total_entries: usize) -> Self {
97        Self {
98            progress,
99            current_entry: 0,
100            total_entries,
101        }
102    }
103
104    /// Reports that processing started for an entry.
105    ///
106    /// Automatically increments the current entry counter and invokes
107    /// the `on_entry_start` callback.
108    ///
109    /// # Parameters
110    ///
111    /// - `path`: Path of the entry being processed
112    pub fn on_entry_start(&mut self, path: &Path) {
113        self.current_entry += 1;
114        self.progress
115            .on_entry_start(path, self.total_entries, self.current_entry);
116    }
117
118    /// Reports that processing completed for an entry.
119    ///
120    /// # Parameters
121    ///
122    /// - `path`: Path of the entry that was processed
123    pub fn on_entry_complete(&mut self, path: &Path) {
124        self.progress.on_entry_complete(path);
125    }
126
127    /// Reports that the entire operation completed.
128    ///
129    /// This should be called exactly once after all entries have been
130    /// processed.
131    pub fn on_complete(&mut self) {
132        self.progress.on_complete();
133    }
134}
135
136/// Wrapper reader that tracks bytes read and reports progress.
137///
138/// This reader wraps any `Read` implementation and reports bytes read
139/// to a progress callback. To reduce callback overhead, it uses batching
140/// with a configurable threshold (default: 1 MB).
141///
142/// # Batching Behavior
143///
144/// The reader accumulates bytes read and only invokes the progress callback
145/// when:
146///
147/// 1. The accumulated bytes reach the batch threshold (default: 1 MB)
148/// 2. The reader is dropped (flushes remaining bytes)
149///
150/// This reduces callback overhead for large files while still providing
151/// responsive progress updates.
152///
153/// # Examples
154///
155/// ```no_run
156/// use exarch_core::ProgressCallback;
157/// use exarch_core::creation::progress::ProgressReader;
158/// use std::fs::File;
159/// use std::io::Read;
160/// use std::path::Path;
161///
162/// # struct DummyProgress;
163/// # impl ProgressCallback for DummyProgress {
164/// #     fn on_entry_start(&mut self, _: &Path, _: usize, _: usize) {}
165/// #     fn on_bytes_written(&mut self, _: u64) {}
166/// #     fn on_entry_complete(&mut self, _: &Path) {}
167/// #     fn on_complete(&mut self) {}
168/// # }
169/// let file = File::open("large_file.bin")?;
170/// let mut progress = DummyProgress;
171/// let mut reader = ProgressReader::new(file, &mut progress);
172///
173/// let mut buffer = vec![0u8; 8192];
174/// loop {
175///     let bytes_read = reader.read(&mut buffer)?;
176///     if bytes_read == 0 {
177///         break;
178///     }
179///     // Progress is automatically reported
180/// }
181/// # Ok::<(), std::io::Error>(())
182/// ```
183pub struct ProgressReader<'a, R> {
184    /// Inner reader being wrapped
185    inner: R,
186    /// Reference to the progress callback
187    progress: &'a mut dyn ProgressCallback,
188    /// Bytes read since last progress update
189    bytes_since_last_update: u64,
190    /// Batch threshold in bytes (default: 1 MB)
191    batch_threshold: u64,
192}
193
194impl<'a, R> ProgressReader<'a, R> {
195    /// Creates a new progress-tracking reader with default batch threshold.
196    ///
197    /// The default batch threshold is 1 MB (1,048,576 bytes).
198    ///
199    /// # Parameters
200    ///
201    /// - `inner`: The reader to wrap
202    /// - `progress`: Mutable reference to the progress callback
203    ///
204    /// # Examples
205    ///
206    /// ```no_run
207    /// use exarch_core::ProgressCallback;
208    /// use exarch_core::creation::progress::ProgressReader;
209    /// use std::fs::File;
210    /// use std::path::Path;
211    ///
212    /// # struct DummyProgress;
213    /// # impl ProgressCallback for DummyProgress {
214    /// #     fn on_entry_start(&mut self, _: &Path, _: usize, _: usize) {}
215    /// #     fn on_bytes_written(&mut self, _: u64) {}
216    /// #     fn on_entry_complete(&mut self, _: &Path) {}
217    /// #     fn on_complete(&mut self) {}
218    /// # }
219    /// let file = File::open("data.bin")?;
220    /// let mut progress = DummyProgress;
221    /// let reader = ProgressReader::new(file, &mut progress);
222    /// # Ok::<(), std::io::Error>(())
223    /// ```
224    #[must_use]
225    pub fn new(inner: R, progress: &'a mut dyn ProgressCallback) -> Self {
226        Self {
227            inner,
228            progress,
229            bytes_since_last_update: 0,
230            batch_threshold: 1024 * 1024, // 1 MB batching threshold
231        }
232    }
233
234    /// Creates a new progress-tracking reader with custom batch threshold.
235    ///
236    /// # Parameters
237    ///
238    /// - `inner`: The reader to wrap
239    /// - `progress`: Mutable reference to the progress callback
240    /// - `batch_threshold`: Number of bytes to accumulate before reporting
241    ///
242    /// # Examples
243    ///
244    /// ```no_run
245    /// use exarch_core::ProgressCallback;
246    /// use exarch_core::creation::progress::ProgressReader;
247    /// use std::fs::File;
248    /// use std::path::Path;
249    ///
250    /// # struct DummyProgress;
251    /// # impl ProgressCallback for DummyProgress {
252    /// #     fn on_entry_start(&mut self, _: &Path, _: usize, _: usize) {}
253    /// #     fn on_bytes_written(&mut self, _: u64) {}
254    /// #     fn on_entry_complete(&mut self, _: &Path) {}
255    /// #     fn on_complete(&mut self) {}
256    /// # }
257    /// let file = File::open("data.bin")?;
258    /// let mut progress = DummyProgress;
259    /// // Report progress every 64 KB
260    /// let reader = ProgressReader::with_batch_threshold(file, &mut progress, 64 * 1024);
261    /// # Ok::<(), std::io::Error>(())
262    /// ```
263    #[must_use]
264    pub fn with_batch_threshold(
265        inner: R,
266        progress: &'a mut dyn ProgressCallback,
267        batch_threshold: u64,
268    ) -> Self {
269        Self {
270            inner,
271            progress,
272            bytes_since_last_update: 0,
273            batch_threshold,
274        }
275    }
276
277    /// Flushes any accumulated bytes to the progress callback.
278    ///
279    /// This is called automatically when the reader is dropped, but can
280    /// be called manually if needed.
281    pub fn flush_progress(&mut self) {
282        if self.bytes_since_last_update > 0 {
283            self.progress.on_bytes_written(self.bytes_since_last_update);
284            self.bytes_since_last_update = 0;
285        }
286    }
287}
288
289impl<R: Read> Read for ProgressReader<'_, R> {
290    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
291        let bytes_read = self.inner.read(buf)?;
292        if bytes_read > 0 {
293            self.bytes_since_last_update += bytes_read as u64;
294            if self.bytes_since_last_update >= self.batch_threshold {
295                self.progress.on_bytes_written(self.bytes_since_last_update);
296                self.bytes_since_last_update = 0;
297            }
298        }
299        Ok(bytes_read)
300    }
301}
302
303impl<R> Drop for ProgressReader<'_, R> {
304    fn drop(&mut self) {
305        self.flush_progress();
306    }
307}
308
309#[cfg(test)]
310#[allow(clippy::unwrap_used, clippy::unused_io_amount)]
311mod tests {
312    use super::*;
313    use std::io::Cursor;
314
315    #[derive(Debug, Default)]
316    struct TestProgress {
317        entries_started: Vec<String>,
318        entries_completed: Vec<String>,
319        bytes_written: u64,
320        completed: bool,
321    }
322
323    impl ProgressCallback for TestProgress {
324        fn on_entry_start(&mut self, path: &Path, _total: usize, _current: usize) {
325            self.entries_started
326                .push(path.to_string_lossy().to_string());
327        }
328
329        fn on_bytes_written(&mut self, bytes: u64) {
330            self.bytes_written += bytes;
331        }
332
333        fn on_entry_complete(&mut self, path: &Path) {
334            self.entries_completed
335                .push(path.to_string_lossy().to_string());
336        }
337
338        fn on_complete(&mut self) {
339            self.completed = true;
340        }
341    }
342
343    #[test]
344    fn test_progress_tracker_entry_counting() {
345        let mut progress = TestProgress::default();
346        let mut tracker = ProgressTracker::new(&mut progress, 3);
347
348        tracker.on_entry_start(Path::new("file1.txt"));
349        tracker.on_entry_complete(Path::new("file1.txt"));
350
351        tracker.on_entry_start(Path::new("file2.txt"));
352        tracker.on_entry_complete(Path::new("file2.txt"));
353
354        tracker.on_complete();
355
356        assert_eq!(progress.entries_started.len(), 2);
357        assert_eq!(progress.entries_completed.len(), 2);
358        assert_eq!(progress.entries_started[0], "file1.txt");
359        assert_eq!(progress.entries_started[1], "file2.txt");
360        assert!(progress.completed);
361    }
362
363    #[test]
364    fn test_progress_reader_reports_bytes() {
365        let data = b"Hello, World!";
366        let reader = Cursor::new(data);
367        let mut progress = TestProgress::default();
368        let mut tracking_reader = ProgressReader::new(reader, &mut progress);
369
370        let mut buffer = vec![0u8; 5];
371        let bytes_read = tracking_reader.read(&mut buffer).unwrap();
372
373        // Drop the reader to flush progress
374        drop(tracking_reader);
375
376        assert_eq!(bytes_read, 5);
377        assert_eq!(progress.bytes_written, 5);
378        assert_eq!(&buffer[..bytes_read], b"Hello");
379    }
380
381    #[test]
382    fn test_progress_reader_batching() {
383        // Create data larger than batch threshold
384        let data = vec![0u8; 2 * 1024 * 1024]; // 2 MB
385        let reader = Cursor::new(data);
386        let mut progress = TestProgress::default();
387
388        // Use small batch threshold for testing
389        let batch_threshold = 64 * 1024; // 64 KB
390        let mut tracking_reader =
391            ProgressReader::with_batch_threshold(reader, &mut progress, batch_threshold);
392
393        let mut buffer = vec![0u8; 32 * 1024]; // 32 KB reads
394
395        // Read multiple times
396        for _ in 0..4 {
397            tracking_reader.read(&mut buffer).unwrap();
398        }
399
400        // Drop reader to flush remaining bytes
401        drop(tracking_reader);
402
403        // Should have reported bytes (batched)
404        assert!(progress.bytes_written > 0);
405    }
406
407    #[test]
408    fn test_progress_reader_handles_eof() {
409        let data = b"";
410        let reader = Cursor::new(data);
411        let mut progress = TestProgress::default();
412        let mut tracking_reader = ProgressReader::new(reader, &mut progress);
413
414        let mut buffer = vec![0u8; 10];
415        let bytes_read = tracking_reader.read(&mut buffer).unwrap();
416
417        // Drop tracking reader before accessing progress
418        drop(tracking_reader);
419
420        assert_eq!(bytes_read, 0);
421        assert_eq!(progress.bytes_written, 0);
422    }
423
424    #[test]
425    fn test_progress_reader_manual_flush() {
426        let data = b"test data";
427        let reader = Cursor::new(data);
428        let mut progress = TestProgress::default();
429
430        let mut buffer = vec![0u8; 4];
431        {
432            let mut tracking_reader = ProgressReader::new(reader, &mut progress);
433
434            tracking_reader.read(&mut buffer).unwrap();
435
436            // Manually flush progress
437            tracking_reader.flush_progress();
438
439            // Reading more shouldn't add to previous bytes
440            tracking_reader.read(&mut buffer).unwrap();
441            tracking_reader.flush_progress();
442        } // Drop tracking_reader here
443
444        // Now we can access progress without borrowing issues
445        assert_eq!(progress.bytes_written, 8); // 4 + 4 = 8
446    }
447}