From 073b5ed418960733f2f0846448e499c836405d64 Mon Sep 17 00:00:00 2001 From: Cameron Date: Mon, 26 Jan 2026 11:42:33 -0500 Subject: [PATCH] Added gps-summary endpoint for Map integration --- src/data/mod.rs | 14 +++++++++ src/database/mod.rs | 71 ++++++++++++++++++++++++++++++++++++++++++ src/files.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 4 +++ 4 files changed, 164 insertions(+) diff --git a/src/data/mod.rs b/src/data/mod.rs index 6c7460c..53e7020 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -347,6 +347,20 @@ pub struct GetTagsRequest { pub path: Option, } +#[derive(Debug, Serialize)] +pub struct GpsPhotoSummary { + pub path: String, + pub lat: f64, + pub lon: f64, + pub date_taken: Option, +} + +#[derive(Debug, Serialize)] +pub struct GpsPhotosResponse { + pub photos: Vec, + pub total: usize, +} + #[cfg(test)] mod tests { use super::Claims; diff --git a/src/database/mod.rs b/src/database/mod.rs index b93e396..7d058d8 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -307,6 +307,15 @@ pub trait ExifDao: Sync + Send { limit: Option, offset: i64, ) -> Result<(Vec, i64), DbError>; + + /// Get all photos with GPS coordinates + /// Returns Vec<(file_path, latitude, longitude, date_taken)> + fn get_all_with_gps( + &mut self, + context: &opentelemetry::Context, + base_path: &str, + recursive: bool, + ) -> Result)>, DbError>; } pub struct SqliteExifDao { @@ -653,4 +662,66 @@ impl ExifDao for SqliteExifDao { }) .map_err(|_| DbError::new(DbErrorKind::QueryError)) } + + fn get_all_with_gps( + &mut self, + context: &opentelemetry::Context, + base_path: &str, + recursive: bool, + ) -> Result)>, DbError> { + trace_db_call(context, "query", "get_all_with_gps", |span| { + use opentelemetry::KeyValue; + use opentelemetry::trace::Span; + use schema::image_exif::dsl::*; + + span.set_attributes(vec![ + KeyValue::new("base_path", base_path.to_string()), + KeyValue::new("recursive", recursive.to_string()), + ]); + + let connection = &mut *self.connection.lock().unwrap(); + + // Query all photos with non-null GPS coordinates + let mut query = image_exif + .filter(gps_latitude.is_not_null().and(gps_longitude.is_not_null())) + .into_boxed(); + + // Apply path filtering + // If base_path is empty or "/", return all GPS photos (no filter) + // Otherwise filter by path prefix + if !base_path.is_empty() && base_path != "/" { + // Match base path as prefix (with wildcard) + query = query.filter(file_path.like(format!("{}%", base_path))); + + span.set_attribute(KeyValue::new("path_filter_applied", true)); + } else { + span.set_attribute(KeyValue::new("path_filter_applied", false)); + span.set_attribute(KeyValue::new("returning_all_gps_photos", true)); + } + + // Load full ImageExif records + let results: Vec = query + .load::(connection) + .map_err(|e| anyhow::anyhow!("GPS query error: {}", e))?; + + // Convert to tuple format (path, lat, lon, date_taken) + // Filter out any rows where GPS is still None (shouldn't happen due to filter) + // Cast f32 GPS values to f64 for API compatibility + let filtered: Vec<(String, f64, f64, Option)> = results + .into_iter() + .filter_map(|exif| { + if let (Some(lat_val), Some(lon_val)) = (exif.gps_latitude, exif.gps_longitude) { + Some((exif.file_path, lat_val as f64, lon_val as f64, exif.date_taken)) + } else { + None + } + }) + .collect(); + + span.set_attribute(KeyValue::new("result_count", filtered.len() as i64)); + + Ok(filtered) + }) + .map_err(|_| DbError::new(DbErrorKind::QueryError)) + } } diff --git a/src/files.rs b/src/files.rs index bfcc754..d925742 100644 --- a/src/files.rs +++ b/src/files.rs @@ -951,6 +951,77 @@ fn is_path_above_base_dir + Debug>( ) } +/// Handler for GPS summary endpoint +/// Returns lightweight GPS data for all photos with coordinates +pub async fn get_gps_summary( + _: Claims, + request: HttpRequest, + req: web::Query, + state: web::Data, + exif_dao: web::Data>>, +) -> Result { + use crate::data::{GpsPhotoSummary, GpsPhotosResponse}; + + let parent_cx = extract_context_from_request(&request); + let tracer = global_tracer(); + let mut span = tracer + .span_builder("get_gps_summary") + .start_with_context(&tracer, &parent_cx); + + span.set_attribute(KeyValue::new("path", req.path.clone())); + span.set_attribute(KeyValue::new("recursive", req.recursive.unwrap_or(false).to_string())); + + let cx = opentelemetry::Context::current_with_span(span); + + // The database stores relative paths, so we use the path as-is + // Normalize empty path or "/" to return all GPS photos + let requested_path = if req.path.is_empty() || req.path == "/" { + "" + } else { + // Just do basic validation to prevent path traversal + if req.path.contains("..") { + warn!("Path traversal attempt: {}", req.path); + cx.span().set_status(Status::error("Invalid path")); + return Ok(HttpResponse::Forbidden().json(serde_json::json!({ + "error": "Invalid path" + }))); + } + req.path.as_str() + }; + + let recursive = req.recursive.unwrap_or(false); + info!("Fetching GPS photos for path='{}' recursive={}", requested_path, recursive); + + // Query database for all photos with GPS + let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao"); + match exif_dao_guard.get_all_with_gps(&cx, requested_path, recursive) { + Ok(gps_data) => { + let photos: Vec = gps_data + .into_iter() + .map(|(path, lat, lon, date_taken)| GpsPhotoSummary { + path, + lat, + lon, + date_taken, + }) + .collect(); + + let total = photos.len(); + cx.span().set_attribute(KeyValue::new("result_count", total as i64)); + cx.span().set_status(Status::Ok); + + Ok(HttpResponse::Ok().json(GpsPhotosResponse { photos, total })) + } + Err(e) => { + error!("Error querying GPS data: {:?}", e); + cx.span().set_status(Status::error(format!("Database error: {:?}", e))); + Ok(HttpResponse::InternalServerError().json(serde_json::json!({ + "error": "Failed to query GPS data" + }))) + } + } +} + pub async fn move_file( _: Claims, file_system: web::Data, @@ -1256,6 +1327,10 @@ mod tests { let count = file_paths.len() as i64; Ok((file_paths.to_vec(), count)) } + + fn get_all_with_gps(&mut self, context: &opentelemetry::Context, base_path: &str, recursive: bool) -> Result)>, DbError> { + todo!() + } } mod api { diff --git a/src/main.rs b/src/main.rs index c2b3161..34a7bf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -853,6 +853,10 @@ fn main() -> std::io::Result<()> { web::resource("/photos") .route(web::get().to(files::list_photos::)), ) + .service( + web::resource("/photos/gps-summary") + .route(web::get().to(files::get_gps_summary)), + ) .service(web::resource("/file/move").post(move_file::)) .service(get_image) .service(upload_image)