use rand::Rng; use std::time::{Duration, SystemTime}; /// Retry a fallible operation with exponential backoff + jitter. /// /// Runs the closure immediately, then retries up to `max_retries` times on /// failure. Each retry sleeps for `base_delay * 2^attempt` plus a uniform /// jitter of ±25%. Non-final errors are logged at `debug` with the label; /// the final error is logged at `error`. /// /// # Examples /// ``` /// use image_api::utils::retry_with_backoff; /// /// let result = retry_with_backoff("my-op", 3, || { /// // something that may fail transiently /// Ok::<_, anyhow::Error>(42) /// }); /// ``` pub fn retry_with_backoff(label: &str, max_retries: u32, mut op: F) -> Result where F: FnMut() -> Result, E: std::fmt::Debug, { let mut last_err = match op() { Ok(v) => return Ok(v), Err(e) => e, }; for attempt in 1..=max_retries { let base = Duration::from_millis(100).saturating_mul(1_u32.pow(attempt - 1)); let jitter_range = base.as_millis() as f64 * 0.25; let jitter = rand::thread_rng().gen_range(-jitter_range..=jitter_range) as u128; let delay = base.as_millis() as i128 + jitter as i128; std::thread::sleep(Duration::from_millis(delay.max(0) as u64)); log::debug!( "{}: attempt {}/{} failed ({:?}), retrying in {}ms", label, attempt, max_retries, last_err, delay.max(0) ); match op() { Ok(v) => return Ok(v), Err(e) => last_err = e, } } log::error!( "{}: all {} retries exhausted: {:?}", label, max_retries, last_err ); Err(last_err) } /// Normalize a file path to use forward slashes for cross-platform consistency /// This ensures paths stored in the database always use `/` regardless of OS /// /// # Examples /// ``` /// use image_api::utils::normalize_path; /// /// assert_eq!(normalize_path("foo\\bar\\baz.jpg"), "foo/bar/baz.jpg"); /// assert_eq!(normalize_path("foo/bar/baz.jpg"), "foo/bar/baz.jpg"); /// ``` pub fn normalize_path(path: &str) -> String { path.replace('\\', "/") } /// Pick the earlier of a file's created and modified timestamps. /// /// On copied/restored files (e.g., a backup library), `created` is stamped at /// copy time while `modified` is preserved from the source — so the earlier /// of the two is a better proxy for when the content originated. Falls back /// to whichever timestamp is available if one platform lacks the other. pub fn earliest_fs_time(md: &std::fs::Metadata) -> Option { match (md.created().ok(), md.modified().ok()) { (Some(c), Some(m)) => Some(c.min(m)), (Some(t), None) | (None, Some(t)) => Some(t), (None, None) => None, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_normalize_path_with_backslashes() { assert_eq!(normalize_path("foo\\bar\\baz.jpg"), "foo/bar/baz.jpg"); } #[test] fn test_normalize_path_with_forward_slashes() { assert_eq!(normalize_path("foo/bar/baz.jpg"), "foo/bar/baz.jpg"); } #[test] fn test_normalize_path_mixed() { assert_eq!( normalize_path("foo\\bar/baz\\qux.jpg"), "foo/bar/baz/qux.jpg" ); } #[test] fn test_normalize_path_empty() { assert_eq!(normalize_path(""), ""); } #[test] fn test_normalize_path_absolute_windows() { assert_eq!( normalize_path("C:\\Users\\Photos\\image.jpg"), "C:/Users/Photos/image.jpg" ); } #[test] fn test_normalize_path_unc_path() { assert_eq!( normalize_path("\\\\server\\share\\folder\\file.jpg"), "//server/share/folder/file.jpg" ); } #[test] fn test_normalize_path_single_filename() { assert_eq!(normalize_path("image.jpg"), "image.jpg"); } #[test] fn test_normalize_path_trailing_slash() { assert_eq!(normalize_path("foo\\bar\\"), "foo/bar/"); } #[test] fn test_normalize_path_multiple_consecutive_backslashes() { assert_eq!( normalize_path("foo\\\\bar\\\\\\baz.jpg"), "foo//bar///baz.jpg" ); } #[test] fn test_normalize_path_deep_nesting() { assert_eq!( normalize_path("a\\b\\c\\d\\e\\f\\g\\file.jpg"), "a/b/c/d/e/f/g/file.jpg" ); } }