Implement critical security improvements for authentication #45
259
src/memories.rs
259
src/memories.rs
@@ -123,63 +123,6 @@ pub struct MemoriesResponse {
|
|||||||
pub items: Vec<MemoryItem>,
|
pub items: Vec<MemoryItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_file_date_info(
|
|
||||||
path: &Path,
|
|
||||||
client_timezone: &Option<FixedOffset>,
|
|
||||||
) -> Option<(NaiveDate, Option<i64>, Option<i64>)> {
|
|
||||||
// Read file metadata once
|
|
||||||
let meta = std::fs::metadata(path).ok()?;
|
|
||||||
|
|
||||||
// Get created timestamp (tries filename first, then metadata)
|
|
||||||
let path_str = path.to_str()?;
|
|
||||||
let created = get_created_timestamp_with_fallback(path_str, &meta, client_timezone);
|
|
||||||
|
|
||||||
// Get modified timestamp from metadata
|
|
||||||
let modified = meta.modified().ok().map(|t| {
|
|
||||||
let utc: DateTime<Utc> = t.into();
|
|
||||||
if let Some(tz) = client_timezone {
|
|
||||||
utc.with_timezone(tz).timestamp()
|
|
||||||
} else {
|
|
||||||
utc.timestamp()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Try to get date from filename for the NaiveDate
|
|
||||||
if let Some(date_time) = path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|filename| filename.to_str())
|
|
||||||
.and_then(extract_date_from_filename)
|
|
||||||
{
|
|
||||||
// Convert to client timezone if specified
|
|
||||||
let date_in_timezone = if let Some(tz) = client_timezone {
|
|
||||||
date_time.with_timezone(tz)
|
|
||||||
} else {
|
|
||||||
date_time.with_timezone(&Local).fixed_offset()
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"File date from file {:?} > {:?} = {:?}",
|
|
||||||
path.file_name(),
|
|
||||||
date_time,
|
|
||||||
date_in_timezone
|
|
||||||
);
|
|
||||||
return Some((date_in_timezone.date_naive(), created, modified));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to metadata if no date in filename
|
|
||||||
let system_time = meta.created().ok().or_else(|| meta.modified().ok())?;
|
|
||||||
let dt_utc: DateTime<Utc> = system_time.into();
|
|
||||||
|
|
||||||
let date_in_timezone = if let Some(tz) = client_timezone {
|
|
||||||
dt_utc.with_timezone(tz).date_naive()
|
|
||||||
} else {
|
|
||||||
dt_utc.with_timezone(&Local).date_naive()
|
|
||||||
};
|
|
||||||
|
|
||||||
trace!("Fallback metadata create date = {:?}", date_in_timezone);
|
|
||||||
Some((date_in_timezone, created, modified))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert Unix timestamp to NaiveDate in client timezone
|
/// Convert Unix timestamp to NaiveDate in client timezone
|
||||||
fn timestamp_to_naive_date(
|
fn timestamp_to_naive_date(
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
@@ -196,37 +139,6 @@ fn timestamp_to_naive_date(
|
|||||||
Some(date)
|
Some(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get created timestamp, trying filename parsing first, then falling back to metadata
|
|
||||||
fn get_created_timestamp_with_fallback(
|
|
||||||
file_path: &str,
|
|
||||||
metadata: &std::fs::Metadata,
|
|
||||||
client_timezone: &Option<FixedOffset>,
|
|
||||||
) -> Option<i64> {
|
|
||||||
// Try to extract date from filename first
|
|
||||||
if let Some(filename_date) = Path::new(file_path)
|
|
||||||
.file_name()
|
|
||||||
.and_then(|f| f.to_str())
|
|
||||||
.and_then(extract_date_from_filename)
|
|
||||||
{
|
|
||||||
let timestamp = if let Some(tz) = client_timezone {
|
|
||||||
filename_date.with_timezone(tz).timestamp()
|
|
||||||
} else {
|
|
||||||
filename_date.timestamp()
|
|
||||||
};
|
|
||||||
return Some(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to metadata
|
|
||||||
metadata.created().ok().map(|t| {
|
|
||||||
let utc: DateTime<Utc> = t.into();
|
|
||||||
if let Some(tz) = client_timezone {
|
|
||||||
utc.with_timezone(tz).timestamp()
|
|
||||||
} else {
|
|
||||||
utc.timestamp()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_date_from_filename(filename: &str) -> Option<DateTime<FixedOffset>> {
|
pub fn extract_date_from_filename(filename: &str) -> Option<DateTime<FixedOffset>> {
|
||||||
let build_date_from_ymd_capture =
|
let build_date_from_ymd_capture =
|
||||||
|captures: ®ex::Captures| -> Option<DateTime<FixedOffset>> {
|
|captures: ®ex::Captures| -> Option<DateTime<FixedOffset>> {
|
||||||
@@ -327,6 +239,99 @@ pub fn extract_date_from_filename(filename: &str) -> Option<DateTime<FixedOffset
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the canonical date for a memory with priority: filename → EXIF → metadata
|
||||||
|
/// Returns (NaiveDate for matching, timestamp for display, modified timestamp)
|
||||||
|
fn get_memory_date_with_priority(
|
||||||
|
path: &Path,
|
||||||
|
exif_date_taken: Option<i64>,
|
||||||
|
client_timezone: &Option<FixedOffset>,
|
||||||
|
) -> Option<(NaiveDate, Option<i64>, Option<i64>)> {
|
||||||
|
// Read file metadata once
|
||||||
|
let meta = std::fs::metadata(path).ok()?;
|
||||||
|
|
||||||
|
// Priority 1: Try to extract date from filename
|
||||||
|
if let Some(filename_date) = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|f| f.to_str())
|
||||||
|
.and_then(extract_date_from_filename)
|
||||||
|
{
|
||||||
|
// Convert to client timezone if specified
|
||||||
|
let date_in_timezone = if let Some(tz) = client_timezone {
|
||||||
|
filename_date.with_timezone(tz)
|
||||||
|
} else {
|
||||||
|
filename_date.with_timezone(&Local).fixed_offset()
|
||||||
|
};
|
||||||
|
|
||||||
|
let timestamp = if let Some(tz) = client_timezone {
|
||||||
|
filename_date.with_timezone(tz).timestamp()
|
||||||
|
} else {
|
||||||
|
filename_date.timestamp()
|
||||||
|
};
|
||||||
|
|
||||||
|
let modified = meta.modified().ok().map(|t| {
|
||||||
|
let utc: DateTime<Utc> = t.into();
|
||||||
|
if let Some(tz) = client_timezone {
|
||||||
|
utc.with_timezone(tz).timestamp()
|
||||||
|
} else {
|
||||||
|
utc.timestamp()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Memory date from filename {:?} > {:?} = {:?}",
|
||||||
|
path.file_name(),
|
||||||
|
filename_date,
|
||||||
|
date_in_timezone
|
||||||
|
);
|
||||||
|
return Some((date_in_timezone.date_naive(), Some(timestamp), modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Use EXIF date_taken if available
|
||||||
|
if let Some(exif_timestamp) = exif_date_taken {
|
||||||
|
let date = timestamp_to_naive_date(exif_timestamp, client_timezone)?;
|
||||||
|
|
||||||
|
let modified = meta.modified().ok().map(|t| {
|
||||||
|
let utc: DateTime<Utc> = t.into();
|
||||||
|
if let Some(tz) = client_timezone {
|
||||||
|
utc.with_timezone(tz).timestamp()
|
||||||
|
} else {
|
||||||
|
utc.timestamp()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!("Memory date from EXIF {:?} = {:?}", path.file_name(), date);
|
||||||
|
return Some((date, Some(exif_timestamp), modified));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Fall back to metadata
|
||||||
|
let system_time = meta.created().ok().or_else(|| meta.modified().ok())?;
|
||||||
|
let dt_utc: DateTime<Utc> = system_time.into();
|
||||||
|
|
||||||
|
let date_in_timezone = if let Some(tz) = client_timezone {
|
||||||
|
dt_utc.with_timezone(tz).date_naive()
|
||||||
|
} else {
|
||||||
|
dt_utc.with_timezone(&Local).date_naive()
|
||||||
|
};
|
||||||
|
|
||||||
|
let created_timestamp = if let Some(tz) = client_timezone {
|
||||||
|
dt_utc.with_timezone(tz).timestamp()
|
||||||
|
} else {
|
||||||
|
dt_utc.timestamp()
|
||||||
|
};
|
||||||
|
|
||||||
|
let modified = meta.modified().ok().map(|t| {
|
||||||
|
let utc: DateTime<Utc> = t.into();
|
||||||
|
if let Some(tz) = client_timezone {
|
||||||
|
utc.with_timezone(tz).timestamp()
|
||||||
|
} else {
|
||||||
|
utc.timestamp()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
trace!("Fallback metadata create date = {:?}", date_in_timezone);
|
||||||
|
Some((date_in_timezone, Some(created_timestamp), modified))
|
||||||
|
}
|
||||||
|
|
||||||
/// Collect memories from EXIF database
|
/// Collect memories from EXIF database
|
||||||
fn collect_exif_memories(
|
fn collect_exif_memories(
|
||||||
exif_dao: &Data<Mutex<Box<dyn ExifDao>>>,
|
exif_dao: &Data<Mutex<Box<dyn ExifDao>>>,
|
||||||
@@ -371,27 +376,16 @@ fn collect_exif_memories(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert timestamp to NaiveDate in client timezone
|
// Get date with priority: filename → EXIF → metadata
|
||||||
let file_date = timestamp_to_naive_date(*date_taken_ts, client_timezone)?;
|
// This ensures sorting and display use the same date source
|
||||||
|
let (file_date, created, modified) =
|
||||||
|
get_memory_date_with_priority(&full_path, Some(*date_taken_ts), client_timezone)?;
|
||||||
|
|
||||||
// Check if matches memory criteria
|
// Check if matches memory criteria
|
||||||
if !is_memories_match(file_path, file_date, now, span_mode, years_back) {
|
if !is_memories_match(file_path, file_date, now, span_mode, years_back) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file metadata for created/modified timestamps
|
|
||||||
let metadata = std::fs::metadata(&full_path).ok()?;
|
|
||||||
let created =
|
|
||||||
get_created_timestamp_with_fallback(file_path, &metadata, client_timezone);
|
|
||||||
let modified = metadata.modified().ok().map(|t| {
|
|
||||||
let utc: DateTime<Utc> = t.into();
|
|
||||||
if let Some(tz) = client_timezone {
|
|
||||||
utc.with_timezone(tz).timestamp()
|
|
||||||
} else {
|
|
||||||
utc.timestamp()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Some((
|
Some((
|
||||||
MemoryItem {
|
MemoryItem {
|
||||||
path: file_path.clone(),
|
path: file_path.clone(),
|
||||||
@@ -440,8 +434,9 @@ fn collect_filesystem_memories(
|
|||||||
entries
|
entries
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
// Use existing get_file_date_info() for filename/metadata fallback
|
// Use unified date priority function (no EXIF for filesystem scan)
|
||||||
let (file_date, created, modified) = get_file_date_info(entry.path(), client_timezone)?;
|
let (file_date, created, modified) =
|
||||||
|
get_memory_date_with_priority(entry.path(), None, client_timezone)?;
|
||||||
|
|
||||||
if is_memories_match(
|
if is_memories_match(
|
||||||
entry.path().to_str().unwrap_or("Unknown"),
|
entry.path().to_str().unwrap_or("Unknown"),
|
||||||
@@ -793,15 +788,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_file_date_info_from_filename() {
|
fn test_memory_date_priority_filename() {
|
||||||
let temp_dir = tempdir().unwrap();
|
let temp_dir = tempdir().unwrap();
|
||||||
let temp_file = temp_dir.path().join("Screenshot_2014-06-01-20-44-50.png");
|
let temp_file = temp_dir.path().join("Screenshot_2014-06-01-20-44-50.png");
|
||||||
File::create(&temp_file).unwrap();
|
File::create(&temp_file).unwrap();
|
||||||
|
|
||||||
let (date, created, _) =
|
// Test that filename takes priority (even with EXIF data available)
|
||||||
get_file_date_info(&temp_file, &Some(*Local::now().fixed_offset().offset())).unwrap();
|
let exif_date = DateTime::<Utc>::from_timestamp(1609459200, 0) // 2021-01-01
|
||||||
|
.unwrap()
|
||||||
|
.timestamp();
|
||||||
|
|
||||||
// Check that date is from filename
|
let (date, created, _) = get_memory_date_with_priority(
|
||||||
|
&temp_file,
|
||||||
|
Some(exif_date),
|
||||||
|
&Some(*Local::now().fixed_offset().offset()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check that date is from filename (2014), NOT EXIF (2021)
|
||||||
assert_eq!(date.year(), 2014);
|
assert_eq!(date.year(), 2014);
|
||||||
assert_eq!(date.month(), 6);
|
assert_eq!(date.month(), 6);
|
||||||
assert_eq!(date.day(), 1);
|
assert_eq!(date.day(), 1);
|
||||||
@@ -820,12 +824,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_file_date_info_from_metadata() {
|
fn test_memory_date_priority_metadata_fallback() {
|
||||||
let temp_dir = tempdir().unwrap();
|
let temp_dir = tempdir().unwrap();
|
||||||
let temp_file = temp_dir.path().join("regular_image.jpg");
|
let temp_file = temp_dir.path().join("regular_image.jpg");
|
||||||
File::create(&temp_file).unwrap();
|
File::create(&temp_file).unwrap();
|
||||||
|
|
||||||
let (date, created, modified) = get_file_date_info(&temp_file, &None).unwrap();
|
// Test metadata fallback when no filename date or EXIF
|
||||||
|
let (date, created, modified) =
|
||||||
|
get_memory_date_with_priority(&temp_file, None, &None).unwrap();
|
||||||
|
|
||||||
// Both date and timestamps should be from metadata (recent)
|
// Both date and timestamps should be from metadata (recent)
|
||||||
let today = Local::now().date_naive();
|
let today = Local::now().date_naive();
|
||||||
@@ -844,6 +850,37 @@ mod tests {
|
|||||||
assert_eq!(dt_modified.year(), today.year());
|
assert_eq!(dt_modified.year(), today.year());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_memory_date_priority_exif_over_metadata() {
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let temp_file = temp_dir.path().join("regular_image.jpg");
|
||||||
|
File::create(&temp_file).unwrap();
|
||||||
|
|
||||||
|
// Test that EXIF takes priority over metadata (but not filename)
|
||||||
|
// EXIF date: June 15, 2020 12:00:00 UTC (safe from timezone edge cases)
|
||||||
|
let exif_date = DateTime::<Utc>::from_timestamp(1592222400, 0) // 2020-06-15 12:00:00 UTC
|
||||||
|
.unwrap()
|
||||||
|
.timestamp();
|
||||||
|
|
||||||
|
let (date, created, modified) =
|
||||||
|
get_memory_date_with_priority(&temp_file, Some(exif_date), &None).unwrap();
|
||||||
|
|
||||||
|
// Date should be from EXIF (2020), not metadata (today)
|
||||||
|
assert_eq!(date.year(), 2020);
|
||||||
|
assert_eq!(date.month(), 6);
|
||||||
|
assert_eq!(date.day(), 15);
|
||||||
|
|
||||||
|
// Created timestamp should also be from EXIF
|
||||||
|
assert!(created.is_some());
|
||||||
|
assert_eq!(created.unwrap(), exif_date);
|
||||||
|
|
||||||
|
// Modified should still be from metadata
|
||||||
|
assert!(modified.is_some());
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
let dt_modified = DateTime::<Utc>::from_timestamp(modified.unwrap(), 0).unwrap();
|
||||||
|
assert_eq!(dt_modified.year(), today.year());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_path_excluder_absolute_under_base() {
|
fn test_path_excluder_absolute_under_base() {
|
||||||
let tmp = tempdir().unwrap();
|
let tmp = tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user