docker_image_pusher/common/
utils.rs1use crate::error::{RegistryError, Result};
7use crate::logging::Logger;
8use std::path::{Path, PathBuf};
9use std::time::{Duration, Instant};
10
11pub struct Timer {
13 start: Instant,
14 description: String,
15}
16
17impl Timer {
18 pub fn start(description: impl Into<String>) -> Self {
20 Self {
21 start: Instant::now(),
22 description: description.into(),
23 }
24 }
25
26 pub fn elapsed(&self) -> Duration {
28 self.start.elapsed()
29 }
30
31 pub fn stop(self) -> Duration {
33 self.elapsed()
34 }
35
36 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
44pub struct PathUtils;
46
47impl PathUtils {
48 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 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 pub fn is_readable_file(path: &Path) -> bool {
68 path.exists() && path.is_file() && std::fs::File::open(path).is_ok()
69 }
70
71 pub fn get_extension(path: &Path) -> Option<&str> {
73 path.extension()?.to_str()
74 }
75
76 pub fn cache_path(cache_dir: &Path, repository: &str, reference: &str) -> PathBuf {
78 cache_dir.join("manifests").join(repository).join(reference)
79 }
80
81 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
91pub struct FormatUtils;
93
94impl FormatUtils {
95 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 pub fn format_speed(bytes_per_sec: u64) -> String {
120 format!("{}/s", Self::format_bytes(bytes_per_sec))
121 }
122
123 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 pub fn format_percentage(value: f64) -> String {
141 format!("{:.1}%", value)
142 }
143
144 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
161pub struct ProgressUtils;
163
164impl ProgressUtils {
165 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 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 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 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
208pub struct ValidationUtils;
210
211impl ValidationUtils {
212 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 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 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 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 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}