Try adding timezone awareness
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{get, web, HttpRequest, HttpResponse, Responder};
|
use actix_web::{get, web, HttpRequest, HttpResponse, Responder};
|
||||||
use chrono::{DateTime, Datelike, Local, NaiveDate, Utc};
|
use chrono::{DateTime, Datelike, FixedOffset, Local, NaiveDate, TimeZone, Utc};
|
||||||
use opentelemetry::trace::{Span, Status, Tracer};
|
use opentelemetry::trace::{Span, Status, Tracer};
|
||||||
use opentelemetry::KeyValue;
|
use opentelemetry::KeyValue;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -23,6 +23,8 @@ pub enum MemoriesSpan {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct MemoriesRequest {
|
pub struct MemoriesRequest {
|
||||||
pub span: Option<MemoriesSpan>,
|
pub span: Option<MemoriesSpan>,
|
||||||
|
/// Client timezone offset in minutes from UTC (e.g., -480 for PST, 60 for CET)
|
||||||
|
pub timezone_offset_minutes: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
@@ -51,7 +53,24 @@ pub async fn list_memories(
|
|||||||
let span_mode = q.span.unwrap_or(MemoriesSpan::Day);
|
let span_mode = q.span.unwrap_or(MemoriesSpan::Day);
|
||||||
let years_back: u32 = 15;
|
let years_back: u32 = 15;
|
||||||
|
|
||||||
let now = Local::now().date_naive();
|
// Create timezone from client offset, default to local timezone if not provided
|
||||||
|
let client_timezone = match q.timezone_offset_minutes {
|
||||||
|
Some(offset_mins) => {
|
||||||
|
let offset_secs = offset_mins * 60;
|
||||||
|
Some(
|
||||||
|
FixedOffset::east_opt(offset_secs)
|
||||||
|
.unwrap_or_else(|| FixedOffset::east_opt(0).unwrap()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = if let Some(tz) = client_timezone {
|
||||||
|
Utc::now().with_timezone(&tz).date_naive()
|
||||||
|
} else {
|
||||||
|
Local::now().date_naive()
|
||||||
|
};
|
||||||
|
|
||||||
let base = Path::new(&app_state.base_path);
|
let base = Path::new(&app_state.base_path);
|
||||||
|
|
||||||
let mut memories_with_dates: Vec<(MemoryItem, NaiveDate)> = Vec::new();
|
let mut memories_with_dates: Vec<(MemoryItem, NaiveDate)> = Vec::new();
|
||||||
@@ -68,7 +87,7 @@ pub async fn list_memories(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use created date if available, otherwise modified date for matching
|
// Use created date if available, otherwise modified date for matching
|
||||||
let file_date = match file_best_local_date(path) {
|
let file_date = match file_best_date(path, &client_timezone) {
|
||||||
Some(d) => d,
|
Some(d) => d,
|
||||||
None => continue,
|
None => continue,
|
||||||
};
|
};
|
||||||
@@ -88,7 +107,8 @@ pub async fn list_memories(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memories_with_dates.sort_by_key(|k| k.1.ordinal());
|
// Sort by day of the month
|
||||||
|
memories_with_dates.sort_by_key(|k| k.1.day());
|
||||||
|
|
||||||
let items: Vec<MemoryItem> = memories_with_dates.into_iter().map(|(m, _)| m).collect();
|
let items: Vec<MemoryItem> = memories_with_dates.into_iter().map(|(m, _)| m).collect();
|
||||||
|
|
||||||
@@ -98,6 +118,13 @@ pub async fn list_memories(
|
|||||||
KeyValue::new("span", format!("{:?}", span_mode)),
|
KeyValue::new("span", format!("{:?}", span_mode)),
|
||||||
KeyValue::new("years_back", years_back.to_string()),
|
KeyValue::new("years_back", years_back.to_string()),
|
||||||
KeyValue::new("result_count", items.len().to_string()),
|
KeyValue::new("result_count", items.len().to_string()),
|
||||||
|
KeyValue::new(
|
||||||
|
"client_timezone",
|
||||||
|
format!(
|
||||||
|
"{:?}",
|
||||||
|
client_timezone.unwrap_or_else(|| FixedOffset::east_opt(0).unwrap())
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
span.set_status(Status::Ok);
|
span.set_status(Status::Ok);
|
||||||
@@ -105,11 +132,19 @@ pub async fn list_memories(
|
|||||||
HttpResponse::Ok().json(MemoriesResponse { items })
|
HttpResponse::Ok().json(MemoriesResponse { items })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_best_local_date(path: &Path) -> Option<NaiveDate> {
|
fn file_best_date(path: &Path, client_timezone: &Option<FixedOffset>) -> Option<NaiveDate> {
|
||||||
let meta = std::fs::metadata(path).ok()?;
|
let meta = std::fs::metadata(path).ok()?;
|
||||||
let system_time = meta.created().ok().or_else(|| meta.modified().ok())?;
|
let system_time = meta.created().ok().or_else(|| meta.modified().ok())?;
|
||||||
let local_dt = chrono::DateTime::<Local>::from(system_time);
|
|
||||||
Some(local_dt.date_naive())
|
let dt = if let Some(tz) = client_timezone {
|
||||||
|
let utc_dt: DateTime<Utc> = system_time.into();
|
||||||
|
utc_dt.with_timezone(tz).date_naive()
|
||||||
|
} else {
|
||||||
|
let local_dt = chrono::DateTime::<Local>::from(system_time);
|
||||||
|
local_dt.date_naive()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_times_epoch_secs(path: &Path) -> (Option<i64>, Option<i64>) {
|
fn file_times_epoch_secs(path: &Path) -> (Option<i64>, Option<i64>) {
|
||||||
@@ -228,4 +263,30 @@ mod tests {
|
|||||||
assert!(!is_memories_match(file_date, today, MemoriesSpan::Day, 10));
|
assert!(!is_memories_match(file_date, today, MemoriesSpan::Day, 10));
|
||||||
assert!(is_memories_match(file_date, today, MemoriesSpan::Day, 20));
|
assert!(is_memories_match(file_date, today, MemoriesSpan::Day, 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timezone_conversion() {
|
||||||
|
// Test file_best_date with different timezones
|
||||||
|
use std::fs::File;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let temp_file = temp_dir.path().join("test_file.jpg");
|
||||||
|
File::create(&temp_file).unwrap();
|
||||||
|
|
||||||
|
// Test with PST (-8 hours = -480 minutes)
|
||||||
|
let pst_offset = FixedOffset::west_opt(8 * 3600).unwrap();
|
||||||
|
let client_tz = Some(pst_offset);
|
||||||
|
|
||||||
|
let date = file_best_date(&temp_file, &client_tz);
|
||||||
|
assert!(date.is_some());
|
||||||
|
|
||||||
|
// Test with no timezone (local)
|
||||||
|
let date_local = file_best_date(&temp_file, &None);
|
||||||
|
assert!(date_local.is_some());
|
||||||
|
|
||||||
|
// Both should return valid dates
|
||||||
|
assert!(date.unwrap().year() >= 2020);
|
||||||
|
assert!(date_local.unwrap().year() >= 2020);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ impl AppState {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn create_test_subdir(base_path: &std::path::Path, name: &str) -> std::path::PathBuf {
|
fn create_test_subdir(base_path: &std::path::Path, name: &str) -> std::path::PathBuf {
|
||||||
let dir_path = base_path.join(name);
|
let dir_path = base_path.join(name);
|
||||||
std::fs::create_dir_all(&dir_path).unwrap_or_else(|_| panic!("Failed to create {} directory", name));
|
std::fs::create_dir_all(&dir_path)
|
||||||
|
.unwrap_or_else(|_| panic!("Failed to create {} directory", name));
|
||||||
dir_path
|
dir_path
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user