docker_image_pusher/common/
utils.rs

1//! Common utilities and helper functions
2//!
3//! This module provides reusable utility functions that can be used across the codebase
4//! to reduce code duplication and improve maintainability.
5
6use crate::error::{RegistryError, Result};
7use crate::logging::Logger;
8use std::path::{Path, PathBuf};
9use std::time::{Duration, Instant};
10
11/// Timing utilities
12pub struct Timer {
13    start: Instant,
14    description: String,
15}
16
17impl Timer {
18    /// Start a new timer
19    pub fn start(description: impl Into<String>) -> Self {
20        Self {
21            start: Instant::now(),
22            description: description.into(),
23        }
24    }
25    
26    /// Get elapsed time
27    pub fn elapsed(&self) -> Duration {
28        self.start.elapsed()
29    }
30    
31    /// Stop timer and return elapsed time
32    pub fn stop(self) -> Duration {
33        self.elapsed()
34    }
35    
36    /// Log elapsed time using provided logger
37    pub fn log_elapsed(&self, logger: &Logger) {
38        logger.info(&format!("{} completed in {:.2}s", 
39                            self.description, 
40                            self.elapsed().as_secs_f64()));
41    }
42}
43
44/// File and path utilities
45pub struct PathUtils;
46
47impl PathUtils {
48    /// Ensure directory exists, create if not
49    pub fn ensure_dir_exists(path: &Path) -> Result<()> {
50        if !path.exists() {
51            std::fs::create_dir_all(path)
52                .map_err(|e| RegistryError::Io(format!("Failed to create directory {}: {}", 
53                                                       path.display(), e)))?;
54        }
55        Ok(())
56    }
57    
58    /// Get file size safely
59    pub fn get_file_size(path: &Path) -> Result<u64> {
60        std::fs::metadata(path)
61            .map(|m| m.len())
62            .map_err(|e| RegistryError::Io(format!("Failed to get file size for {}: {}", 
63                                                   path.display(), e)))
64    }
65    
66    /// Check if file exists and is readable
67    pub fn is_readable_file(path: &Path) -> bool {
68        path.exists() && path.is_file() && std::fs::File::open(path).is_ok()
69    }
70    
71    /// Get file extension safely
72    pub fn get_extension(path: &Path) -> Option<&str> {
73        path.extension()?.to_str()
74    }
75    
76    /// Construct cache path for repository/reference
77    pub fn cache_path(cache_dir: &Path, repository: &str, reference: &str) -> PathBuf {
78        cache_dir.join("manifests").join(repository).join(reference)
79    }
80    
81    /// Construct blob path for digest
82    pub fn blob_path(cache_dir: &Path, digest: &str) -> PathBuf {
83        if digest.starts_with("sha256:") {
84            cache_dir.join("blobs").join("sha256").join(&digest[7..])
85        } else {
86            cache_dir.join("blobs").join("sha256").join(digest)
87        }
88    }
89}
90
91/// Format utilities
92pub struct FormatUtils;
93
94impl FormatUtils {
95    /// Format bytes as human readable size
96    pub fn format_bytes(bytes: u64) -> String {
97        const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
98        
99        if bytes == 0 {
100            return "0 B".to_string();
101        }
102        
103        let mut size = bytes as f64;
104        let mut unit_index = 0;
105        
106        while size >= 1024.0 && unit_index < UNITS.len() - 1 {
107            size /= 1024.0;
108            unit_index += 1;
109        }
110        
111        if unit_index == 0 {
112            format!("{} {}", size as u64, UNITS[unit_index])
113        } else {
114            format!("{:.2} {}", size, UNITS[unit_index])
115        }
116    }
117    
118    /// Format speed in bytes/sec
119    pub fn format_speed(bytes_per_sec: u64) -> String {
120        format!("{}/s", Self::format_bytes(bytes_per_sec))
121    }
122    
123    /// Format duration as human readable
124    pub fn format_duration(duration: Duration) -> String {
125        let total_secs = duration.as_secs();
126        let hours = total_secs / 3600;
127        let minutes = (total_secs % 3600) / 60;
128        let seconds = total_secs % 60;
129        
130        if hours > 0 {
131            format!("{}h{}m{}s", hours, minutes, seconds)
132        } else if minutes > 0 {
133            format!("{}m{}s", minutes, seconds)
134        } else {
135            format!("{}s", seconds)
136        }
137    }
138    
139    /// Format percentage
140    pub fn format_percentage(value: f64) -> String {
141        format!("{:.1}%", value)
142    }
143    
144    /// Truncate digest for display
145    pub fn truncate_digest(digest: &str, len: usize) -> String {
146        if digest.starts_with("sha256:") {
147            let hash_part = &digest[7..];
148            if hash_part.len() > len {
149                format!("sha256:{}...", &hash_part[..len])
150            } else {
151                digest.to_string()
152            }
153        } else if digest.len() > len {
154            format!("{}...", &digest[..len])
155        } else {
156            digest.to_string()
157        }
158    }
159}
160
161/// Progress calculation utilities
162pub struct ProgressUtils;
163
164impl ProgressUtils {
165    /// Calculate progress percentage
166    pub fn calculate_percentage(processed: u64, total: u64) -> f64 {
167        if total == 0 {
168            0.0
169        } else {
170            (processed as f64 / total as f64) * 100.0
171        }
172    }
173    
174    /// Calculate speed (bytes per second)
175    pub fn calculate_speed(bytes: u64, duration: Duration) -> u64 {
176        let secs = duration.as_secs_f64();
177        if secs > 0.0 {
178            (bytes as f64 / secs) as u64
179        } else {
180            0
181        }
182    }
183    
184    /// Estimate remaining time
185    pub fn estimate_remaining_time(processed: u64, total: u64, elapsed: Duration) -> Option<Duration> {
186        if processed == 0 || processed >= total {
187            return None;
188        }
189        
190        let rate = processed as f64 / elapsed.as_secs_f64();
191        let remaining_bytes = total - processed;
192        let remaining_secs = remaining_bytes as f64 / rate;
193        
194        Some(Duration::from_secs_f64(remaining_secs))
195    }
196    
197    /// Create progress bar string
198    pub fn create_progress_bar(percentage: f64, width: usize) -> String {
199        let filled = ((percentage / 100.0) * width as f64) as usize;
200        let empty = width.saturating_sub(filled);
201        
202        format!("[{}{}]", 
203                "█".repeat(filled), 
204                "░".repeat(empty))
205    }
206}
207
208/// Validation utilities
209pub struct ValidationUtils;
210
211impl ValidationUtils {
212    /// Validate repository name
213    pub fn validate_repository(repository: &str) -> Result<()> {
214        if repository.is_empty() {
215            return Err(RegistryError::Validation("Repository cannot be empty".to_string()));
216        }
217        
218        // Basic repository name validation
219        if repository.contains("//") || repository.starts_with('/') || repository.ends_with('/') {
220            return Err(RegistryError::Validation(
221                "Invalid repository format".to_string()
222            ));
223        }
224        
225        Ok(())
226    }
227    
228    /// Validate reference (tag or digest)
229    pub fn validate_reference(reference: &str) -> Result<()> {
230        if reference.is_empty() {
231            return Err(RegistryError::Validation("Reference cannot be empty".to_string()));
232        }
233        
234        // Basic reference validation
235        if reference.contains(' ') || reference.contains('\t') || reference.contains('\n') {
236            return Err(RegistryError::Validation(
237                "Reference cannot contain whitespace".to_string()
238            ));
239        }
240        
241        Ok(())
242    }
243    
244    /// Validate digest format
245    pub fn validate_digest(digest: &str) -> Result<()> {
246        if !digest.starts_with("sha256:") {
247            return Err(RegistryError::Validation(
248                "Digest must start with 'sha256:'".to_string()
249            ));
250        }
251        
252        let hash_part = &digest[7..];
253        if hash_part.len() != 64 {
254            return Err(RegistryError::Validation(
255                "SHA256 digest must be 64 characters".to_string()
256            ));
257        }
258        
259        if !hash_part.chars().all(|c| c.is_ascii_hexdigit()) {
260            return Err(RegistryError::Validation(
261                "Digest must contain only hexadecimal characters".to_string()
262            ));
263        }
264        
265        Ok(())
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use std::time::Duration;
273
274    #[test]
275    fn test_format_bytes() {
276        assert_eq!(FormatUtils::format_bytes(0), "0 B");
277        assert_eq!(FormatUtils::format_bytes(1024), "1.00 KB");
278        assert_eq!(FormatUtils::format_bytes(1536), "1.50 KB");
279        assert_eq!(FormatUtils::format_bytes(1048576), "1.00 MB");
280    }
281
282    #[test]
283    fn test_progress_percentage() {
284        assert_eq!(ProgressUtils::calculate_percentage(0, 100), 0.0);
285        assert_eq!(ProgressUtils::calculate_percentage(50, 100), 50.0);
286        assert_eq!(ProgressUtils::calculate_percentage(100, 100), 100.0);
287        assert_eq!(ProgressUtils::calculate_percentage(0, 0), 0.0);
288    }
289
290    #[test]
291    fn test_validate_repository() {
292        assert!(ValidationUtils::validate_repository("valid/repo").is_ok());
293        assert!(ValidationUtils::validate_repository("").is_err());
294        assert!(ValidationUtils::validate_repository("//invalid").is_err());
295        assert!(ValidationUtils::validate_repository("/invalid").is_err());
296    }
297
298    #[test]
299    fn test_validate_digest() {
300        let valid_digest = "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
301        assert!(ValidationUtils::validate_digest(valid_digest).is_ok());
302        assert!(ValidationUtils::validate_digest("invalid").is_err());
303        assert!(ValidationUtils::validate_digest("sha256:invalid").is_err());
304    }
305
306    #[test]
307    fn test_timer() {
308        let timer = Timer::start("test operation");
309        std::thread::sleep(Duration::from_millis(10));
310        assert!(timer.elapsed() >= Duration::from_millis(10));
311    }
312}