diff --git a/src/ai/insight_generator.rs b/src/ai/insight_generator.rs index 8a0ed11..dad7040 100644 --- a/src/ai/insight_generator.rs +++ b/src/ai/insight_generator.rs @@ -23,7 +23,7 @@ use crate::libraries::Library; use crate::memories::extract_date_from_filename; use crate::otel::global_tracer; use crate::tags::TagDao; -use crate::utils::normalize_path; +use crate::utils::{earliest_fs_time, normalize_path}; #[derive(Deserialize)] struct NominatimResponse { @@ -760,8 +760,6 @@ impl InsightGenerator { let full_path = self.resolve_full_path(&file_path)?; File::open(&full_path) .and_then(|f| f.metadata()) - .and_then(|m| m.created().or(m.modified())) - .map(|t| DateTime::::from(t).timestamp()) .inspect_err(|e| { log::warn!( "Failed to get file timestamp for insight {}: {}", @@ -770,6 +768,8 @@ impl InsightGenerator { ) }) .ok() + .and_then(|m| earliest_fs_time(&m)) + .map(|t| DateTime::::from(t).timestamp()) }) .unwrap_or_else(|| Utc::now().timestamp()) }; @@ -1687,7 +1687,11 @@ Return ONLY the summary, nothing else."#, let date = chrono::DateTime::from_timestamp(h.date, 0) .map(|dt| dt.format("%Y-%m-%d").to_string()) .unwrap_or_else(|| h.date.to_string()); - let direction: &str = if h.type_ == 2 { &user_name } else { &h.contact_name }; + let direction: &str = if h.type_ == 2 { + &user_name + } else { + &h.contact_name + }; let score = h .similarity_score .map(|s| format!(" [score {:.2}]", s)) @@ -2858,8 +2862,6 @@ Return ONLY the summary, nothing else."#, let full_path = self.resolve_full_path(&file_path)?; File::open(&full_path) .and_then(|f| f.metadata()) - .and_then(|m| m.created().or(m.modified())) - .map(|t| DateTime::::from(t).timestamp()) .inspect_err(|e| { log::warn!( "Failed to get file timestamp for agentic insight {}: {}", @@ -2868,6 +2870,8 @@ Return ONLY the summary, nothing else."#, ) }) .ok() + .and_then(|m| earliest_fs_time(&m)) + .map(|t| DateTime::::from(t).timestamp()) }) .unwrap_or_else(|| Utc::now().timestamp()) }; diff --git a/src/files.rs b/src/files.rs index 561414c..75343b1 100644 --- a/src/files.rs +++ b/src/files.rs @@ -15,6 +15,7 @@ use crate::database::ExifDao; use crate::file_types; use crate::geo::{gps_bounding_box, haversine_distance}; use crate::memories::extract_date_from_filename; +use crate::utils::earliest_fs_time; use crate::{AppState, create_thumbnails}; use actix_web::web::Data; use actix_web::{ @@ -138,8 +139,8 @@ fn in_memory_date_sort( lib_roots.get(&lib_id).and_then(|root| { let full_path = Path::new(root).join(&f.file_name); std::fs::metadata(full_path) - .and_then(|md| md.created().or(md.modified())) .ok() + .and_then(|md| earliest_fs_time(&md)) .map(|system_time| { >>::into(system_time).timestamp() }) diff --git a/src/memories.rs b/src/memories.rs index 875a72c..0e2aad5 100644 --- a/src/memories.rs +++ b/src/memories.rs @@ -19,6 +19,7 @@ use crate::files::is_image_or_video; use crate::libraries::Library; use crate::otel::{extract_context_from_request, global_tracer}; use crate::state::AppState; +use crate::utils::earliest_fs_time; // Helper that encapsulates path-exclusion semantics #[derive(Debug)] @@ -336,8 +337,8 @@ fn get_memory_date_with_priority( return Some((date, Some(exif_timestamp), modified)); } - // Priority 3: Fall back to metadata - let system_time = meta.created().ok().or_else(|| meta.modified().ok())?; + // Priority 3: Fall back to metadata (earlier of created/modified — see utils::earliest_fs_time) + let system_time = earliest_fs_time(&meta)?; let dt_utc: DateTime = system_time.into(); let date_in_timezone = if let Some(tz) = client_timezone { diff --git a/src/utils.rs b/src/utils.rs index 1779c15..fdfef9b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,5 @@ +use std::time::SystemTime; + /// Normalize a file path to use forward slashes for cross-platform consistency /// This ensures paths stored in the database always use `/` regardless of OS /// @@ -12,6 +14,20 @@ 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::*;