feature/memories #35
@@ -1,6 +1,6 @@
|
||||
use actix_web::web::Data;
|
||||
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::KeyValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -23,6 +23,8 @@ pub enum MemoriesSpan {
|
||||
#[derive(Deserialize)]
|
||||
pub struct MemoriesRequest {
|
||||
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)]
|
||||
@@ -51,7 +53,24 @@ pub async fn list_memories(
|
||||
let span_mode = q.span.unwrap_or(MemoriesSpan::Day);
|
||||
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 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
|
||||
let file_date = match file_best_local_date(path) {
|
||||
let file_date = match file_best_date(path, &client_timezone) {
|
||||
Some(d) => d,
|
||||
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();
|
||||
|
||||
@@ -98,6 +118,13 @@ pub async fn list_memories(
|
||||
KeyValue::new("span", format!("{:?}", span_mode)),
|
||||
KeyValue::new("years_back", years_back.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);
|
||||
@@ -105,11 +132,19 @@ pub async fn list_memories(
|
||||
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 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>) {
|
||||
@@ -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, 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)]
|
||||
fn create_test_subdir(base_path: &std::path::Path, name: &str) -> std::path::PathBuf {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user