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}