Added gps-summary endpoint for Map integration
This commit is contained in:
@@ -347,6 +347,20 @@ pub struct GetTagsRequest {
|
|||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct GpsPhotoSummary {
|
||||||
|
pub path: String,
|
||||||
|
pub lat: f64,
|
||||||
|
pub lon: f64,
|
||||||
|
pub date_taken: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct GpsPhotosResponse {
|
||||||
|
pub photos: Vec<GpsPhotoSummary>,
|
||||||
|
pub total: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::Claims;
|
use super::Claims;
|
||||||
|
|||||||
@@ -307,6 +307,15 @@ pub trait ExifDao: Sync + Send {
|
|||||||
limit: Option<i64>,
|
limit: Option<i64>,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
) -> Result<(Vec<String>, i64), DbError>;
|
) -> Result<(Vec<String>, 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<Vec<(String, f64, f64, Option<i64>)>, DbError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SqliteExifDao {
|
pub struct SqliteExifDao {
|
||||||
@@ -653,4 +662,66 @@ impl ExifDao for SqliteExifDao {
|
|||||||
})
|
})
|
||||||
.map_err(|_| DbError::new(DbErrorKind::QueryError))
|
.map_err(|_| DbError::new(DbErrorKind::QueryError))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_all_with_gps(
|
||||||
|
&mut self,
|
||||||
|
context: &opentelemetry::Context,
|
||||||
|
base_path: &str,
|
||||||
|
recursive: bool,
|
||||||
|
) -> Result<Vec<(String, f64, f64, Option<i64>)>, 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<ImageExif> = query
|
||||||
|
.load::<ImageExif>(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<i64>)> = 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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/files.rs
75
src/files.rs
@@ -951,6 +951,77 @@ fn is_path_above_base_dir<P: AsRef<Path> + 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<FilesRequest>,
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
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<GpsPhotoSummary> = 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<FS: FileSystemAccess>(
|
pub async fn move_file<FS: FileSystemAccess>(
|
||||||
_: Claims,
|
_: Claims,
|
||||||
file_system: web::Data<FS>,
|
file_system: web::Data<FS>,
|
||||||
@@ -1256,6 +1327,10 @@ mod tests {
|
|||||||
let count = file_paths.len() as i64;
|
let count = file_paths.len() as i64;
|
||||||
Ok((file_paths.to_vec(), count))
|
Ok((file_paths.to_vec(), count))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_all_with_gps(&mut self, context: &opentelemetry::Context, base_path: &str, recursive: bool) -> Result<Vec<(String, f64, f64, Option<i64>)>, DbError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod api {
|
mod api {
|
||||||
|
|||||||
@@ -853,6 +853,10 @@ fn main() -> std::io::Result<()> {
|
|||||||
web::resource("/photos")
|
web::resource("/photos")
|
||||||
.route(web::get().to(files::list_photos::<SqliteTagDao, RealFileSystem>)),
|
.route(web::get().to(files::list_photos::<SqliteTagDao, RealFileSystem>)),
|
||||||
)
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/photos/gps-summary")
|
||||||
|
.route(web::get().to(files::get_gps_summary)),
|
||||||
|
)
|
||||||
.service(web::resource("/file/move").post(move_file::<RealFileSystem>))
|
.service(web::resource("/file/move").post(move_file::<RealFileSystem>))
|
||||||
.service(get_image)
|
.service(get_image)
|
||||||
.service(upload_image)
|
.service(upload_image)
|
||||||
|
|||||||
Reference in New Issue
Block a user