feature/exif-endpoint #44
338
src/files.rs
338
src/files.rs
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::fs::read_dir;
|
use std::fs::read_dir;
|
||||||
use std::io;
|
use std::io;
|
||||||
@@ -9,14 +10,17 @@ use ::anyhow;
|
|||||||
use actix::{Handler, Message};
|
use actix::{Handler, Message};
|
||||||
use anyhow::{Context, anyhow};
|
use anyhow::{Context, anyhow};
|
||||||
|
|
||||||
use crate::data::{Claims, FilesRequest, FilterMode, PhotosResponse, SortType};
|
use crate::data::{Claims, FilesRequest, FilterMode, MediaType, PhotosResponse, SortType};
|
||||||
|
use crate::database::ExifDao;
|
||||||
|
use crate::geo::{gps_bounding_box, haversine_distance};
|
||||||
|
use crate::memories::extract_date_from_filename;
|
||||||
use crate::{AppState, create_thumbnails};
|
use crate::{AppState, create_thumbnails};
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
HttpRequest, HttpResponse,
|
HttpRequest, HttpResponse,
|
||||||
web::{self, Query},
|
web::{self, Query},
|
||||||
};
|
};
|
||||||
use log::{debug, error, info, trace};
|
use log::{debug, error, info, trace, warn};
|
||||||
use opentelemetry::KeyValue;
|
use opentelemetry::KeyValue;
|
||||||
use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer};
|
use opentelemetry::trace::{Span, Status, TraceContextExt, Tracer};
|
||||||
|
|
||||||
@@ -45,6 +49,7 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
app_state: web::Data<AppState>,
|
app_state: web::Data<AppState>,
|
||||||
file_system: web::Data<FS>,
|
file_system: web::Data<FS>,
|
||||||
tag_dao: web::Data<Mutex<TagD>>,
|
tag_dao: web::Data<Mutex<TagD>>,
|
||||||
|
exif_dao: web::Data<Mutex<Box<dyn ExifDao>>>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let search_path = &req.path;
|
let search_path = &req.path;
|
||||||
|
|
||||||
@@ -67,10 +72,119 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
req.exclude_tag_ids.clone().unwrap_or_default().to_string(),
|
req.exclude_tag_ids.clone().unwrap_or_default().to_string(),
|
||||||
),
|
),
|
||||||
KeyValue::new("sort", format!("{:?}", &req.sort.unwrap_or(NameAsc))),
|
KeyValue::new("sort", format!("{:?}", &req.sort.unwrap_or(NameAsc))),
|
||||||
|
// EXIF search parameters
|
||||||
|
KeyValue::new("camera_make", req.camera_make.clone().unwrap_or_default()),
|
||||||
|
KeyValue::new("camera_model", req.camera_model.clone().unwrap_or_default()),
|
||||||
|
KeyValue::new("lens_model", req.lens_model.clone().unwrap_or_default()),
|
||||||
|
KeyValue::new(
|
||||||
|
"gps_lat",
|
||||||
|
req.gps_lat.map(|v| v.to_string()).unwrap_or_default(),
|
||||||
|
),
|
||||||
|
KeyValue::new(
|
||||||
|
"gps_lon",
|
||||||
|
req.gps_lon.map(|v| v.to_string()).unwrap_or_default(),
|
||||||
|
),
|
||||||
|
KeyValue::new(
|
||||||
|
"gps_radius_km",
|
||||||
|
req.gps_radius_km.map(|v| v.to_string()).unwrap_or_default(),
|
||||||
|
),
|
||||||
|
KeyValue::new(
|
||||||
|
"date_from",
|
||||||
|
req.date_from.map(|v| v.to_string()).unwrap_or_default(),
|
||||||
|
),
|
||||||
|
KeyValue::new(
|
||||||
|
"date_to",
|
||||||
|
req.date_to.map(|v| v.to_string()).unwrap_or_default(),
|
||||||
|
),
|
||||||
|
KeyValue::new(
|
||||||
|
"media_type",
|
||||||
|
req.media_type
|
||||||
|
.as_ref()
|
||||||
|
.map(|mt| format!("{:?}", mt))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let span_context = opentelemetry::Context::current_with_span(span);
|
let span_context = opentelemetry::Context::current_with_span(span);
|
||||||
|
|
||||||
|
// Check if EXIF filtering is requested
|
||||||
|
let has_exif_filters = req.camera_make.is_some()
|
||||||
|
|| req.camera_model.is_some()
|
||||||
|
|| req.lens_model.is_some()
|
||||||
|
|| req.gps_lat.is_some()
|
||||||
|
|| req.date_from.is_some()
|
||||||
|
|| req.date_to.is_some();
|
||||||
|
|
||||||
|
// Apply EXIF-based filtering if requested
|
||||||
|
let exif_matched_files: Option<HashSet<String>> = if has_exif_filters {
|
||||||
|
// Validate GPS parameters (all 3 must be present together)
|
||||||
|
if (req.gps_lat.is_some() || req.gps_lon.is_some() || req.gps_radius_km.is_some())
|
||||||
|
&& !(req.gps_lat.is_some() && req.gps_lon.is_some() && req.gps_radius_km.is_some())
|
||||||
|
{
|
||||||
|
warn!("GPS search requires lat, lon, and radius_km to all be specified");
|
||||||
|
span_context
|
||||||
|
.span()
|
||||||
|
.set_status(Status::error("Invalid GPS parameters"));
|
||||||
|
return HttpResponse::BadRequest().body("GPS search requires lat, lon, and radius_km");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate GPS bounding box if GPS search is requested
|
||||||
|
let gps_bounds = if let (Some(lat), Some(lon), Some(radius_km)) =
|
||||||
|
(req.gps_lat, req.gps_lon, req.gps_radius_km)
|
||||||
|
{
|
||||||
|
let (min_lat, max_lat, min_lon, max_lon) = gps_bounding_box(lat, lon, radius_km);
|
||||||
|
Some((min_lat, max_lat, min_lon, max_lon))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Query EXIF database
|
||||||
|
let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao");
|
||||||
|
let exif_results = exif_dao_guard
|
||||||
|
.query_by_exif(
|
||||||
|
req.camera_make.as_deref(),
|
||||||
|
req.camera_model.as_deref(),
|
||||||
|
req.lens_model.as_deref(),
|
||||||
|
gps_bounds,
|
||||||
|
req.date_from,
|
||||||
|
req.date_to,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
warn!("EXIF query failed: {:?}", e);
|
||||||
|
Vec::new()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply precise GPS distance filtering if GPS search was requested
|
||||||
|
let filtered_results = if let (Some(lat), Some(lon), Some(radius_km)) =
|
||||||
|
(req.gps_lat, req.gps_lon, req.gps_radius_km)
|
||||||
|
{
|
||||||
|
exif_results
|
||||||
|
.into_iter()
|
||||||
|
.filter(|exif| {
|
||||||
|
if let (Some(photo_lat), Some(photo_lon)) =
|
||||||
|
(exif.gps_latitude, exif.gps_longitude)
|
||||||
|
{
|
||||||
|
let distance = haversine_distance(lat, lon, photo_lat, photo_lon);
|
||||||
|
distance <= radius_km
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|exif| exif.file_path)
|
||||||
|
.collect::<HashSet<String>>()
|
||||||
|
} else {
|
||||||
|
exif_results
|
||||||
|
.into_iter()
|
||||||
|
.map(|exif| exif.file_path)
|
||||||
|
.collect::<HashSet<String>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("EXIF filtering matched {} files", filtered_results.len());
|
||||||
|
Some(filtered_results)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let search_recursively = req.recursive.unwrap_or(false);
|
let search_recursively = req.recursive.unwrap_or(false);
|
||||||
if let Some(tag_ids) = &req.tag_ids
|
if let Some(tag_ids) = &req.tag_ids
|
||||||
&& search_recursively
|
&& search_recursively
|
||||||
@@ -128,6 +242,14 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
search_path.strip_suffix('/').unwrap_or_else(|| search_path)
|
search_path.strip_suffix('/').unwrap_or_else(|| search_path)
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
.filter(|f| {
|
||||||
|
// Apply EXIF filtering if present
|
||||||
|
if let Some(ref exif_files) = exif_matched_files {
|
||||||
|
exif_files.contains(&f.file_name)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect::<Vec<FileWithTagCount>>()
|
.collect::<Vec<FileWithTagCount>>()
|
||||||
})
|
})
|
||||||
.map(|files| sort(files, req.sort.unwrap_or(NameAsc)))
|
.map(|files| sort(files, req.sort.unwrap_or(NameAsc)))
|
||||||
@@ -211,21 +333,83 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
|||||||
|
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
|
.filter(|(file_name, _)| {
|
||||||
|
// Apply EXIF filtering if present
|
||||||
|
if let Some(ref exif_files) = exif_matched_files {
|
||||||
|
exif_files.contains(file_name)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(|(file_name, _)| {
|
||||||
|
// Apply media type filtering if specified
|
||||||
|
if let Some(ref media_type) = req.media_type {
|
||||||
|
let path = PathBuf::from(file_name);
|
||||||
|
matches_media_type(&path, media_type)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
.map(|(file_name, tags)| FileWithTagCount {
|
.map(|(file_name, tags)| FileWithTagCount {
|
||||||
file_name,
|
file_name,
|
||||||
tag_count: tags.len() as i64,
|
tag_count: tags.len() as i64,
|
||||||
})
|
})
|
||||||
.collect::<Vec<FileWithTagCount>>();
|
.collect::<Vec<FileWithTagCount>>();
|
||||||
|
|
||||||
let mut response_files = photos
|
// Handle sorting - use FileWithMetadata for date sorting to support EXIF dates
|
||||||
.clone()
|
let response_files = if let Some(sort_type) = req.sort {
|
||||||
|
match sort_type {
|
||||||
|
SortType::DateTakenAsc | SortType::DateTakenDesc => {
|
||||||
|
debug!("Date sorting requested, fetching EXIF data");
|
||||||
|
|
||||||
|
// Collect file paths for batch EXIF query
|
||||||
|
let file_paths: Vec<String> =
|
||||||
|
photos.iter().map(|f| f.file_name.clone()).collect();
|
||||||
|
|
||||||
|
// Batch fetch EXIF data
|
||||||
|
let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao");
|
||||||
|
let exif_map: std::collections::HashMap<String, i64> = exif_dao_guard
|
||||||
|
.get_exif_batch(&file_paths)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|exif| exif.date_taken.map(|dt| (exif.file_path, dt)))
|
||||||
|
.collect();
|
||||||
|
drop(exif_dao_guard);
|
||||||
|
|
||||||
|
// Convert to FileWithMetadata with date fallback logic
|
||||||
|
let files_with_metadata: Vec<FileWithMetadata> = photos
|
||||||
|
.into_iter()
|
||||||
|
.map(|f| {
|
||||||
|
// Try EXIF date first
|
||||||
|
let date_taken =
|
||||||
|
exif_map.get(&f.file_name).copied().or_else(|| {
|
||||||
|
// Fallback to filename extraction
|
||||||
|
extract_date_from_filename(&f.file_name)
|
||||||
|
.map(|dt| dt.timestamp())
|
||||||
|
});
|
||||||
|
|
||||||
|
FileWithMetadata {
|
||||||
|
file_name: f.file_name,
|
||||||
|
tag_count: f.tag_count,
|
||||||
|
date_taken,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
sort_with_metadata(files_with_metadata, sort_type)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Use regular sort for non-date sorting
|
||||||
|
sort(photos, sort_type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No sorting requested
|
||||||
|
photos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|f| f.file_name)
|
.map(|f| f.file_name)
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>()
|
||||||
if let Some(sort_type) = req.sort {
|
};
|
||||||
debug!("Sorting files: {:?}", sort_type);
|
|
||||||
response_files = sort(photos, sort_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
let dirs = files
|
let dirs = files
|
||||||
.iter()
|
.iter()
|
||||||
@@ -369,6 +553,31 @@ pub fn is_image_or_video(path: &Path) -> bool {
|
|||||||
|| extension == "avif"
|
|| extension == "avif"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a file matches the media type filter
|
||||||
|
fn matches_media_type(path: &Path, media_type: &MediaType) -> bool {
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.and_then(|p| p.to_str())
|
||||||
|
.map_or(String::from(""), |p| p.to_lowercase());
|
||||||
|
|
||||||
|
match media_type {
|
||||||
|
MediaType::All => true,
|
||||||
|
MediaType::Photo => {
|
||||||
|
extension == "png"
|
||||||
|
|| extension == "jpg"
|
||||||
|
|| extension == "jpeg"
|
||||||
|
|| extension == "nef"
|
||||||
|
|| extension == "webp"
|
||||||
|
|| extension == "tiff"
|
||||||
|
|| extension == "tif"
|
||||||
|
|| extension == "heif"
|
||||||
|
|| extension == "heic"
|
||||||
|
|| extension == "avif"
|
||||||
|
}
|
||||||
|
MediaType::Video => extension == "mp4" || extension == "mov",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_valid_full_path<P: AsRef<Path> + Debug + AsRef<std::ffi::OsStr>>(
|
pub fn is_valid_full_path<P: AsRef<Path> + Debug + AsRef<std::ffi::OsStr>>(
|
||||||
base: &P,
|
base: &P,
|
||||||
path: &P,
|
path: &P,
|
||||||
@@ -571,6 +780,104 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct MockExifDao;
|
||||||
|
|
||||||
|
impl crate::database::ExifDao for MockExifDao {
|
||||||
|
fn store_exif(
|
||||||
|
&mut self,
|
||||||
|
data: crate::database::models::InsertImageExif,
|
||||||
|
) -> Result<crate::database::models::ImageExif, crate::database::DbError> {
|
||||||
|
// Return a dummy ImageExif for tests
|
||||||
|
Ok(crate::database::models::ImageExif {
|
||||||
|
id: 1,
|
||||||
|
file_path: data.file_path.to_string(),
|
||||||
|
camera_make: data.camera_make.map(|s| s.to_string()),
|
||||||
|
camera_model: data.camera_model.map(|s| s.to_string()),
|
||||||
|
lens_model: data.lens_model.map(|s| s.to_string()),
|
||||||
|
width: data.width,
|
||||||
|
height: data.height,
|
||||||
|
orientation: data.orientation,
|
||||||
|
gps_latitude: data.gps_latitude,
|
||||||
|
gps_longitude: data.gps_longitude,
|
||||||
|
gps_altitude: data.gps_altitude,
|
||||||
|
focal_length: data.focal_length,
|
||||||
|
aperture: data.aperture,
|
||||||
|
shutter_speed: data.shutter_speed,
|
||||||
|
iso: data.iso,
|
||||||
|
date_taken: data.date_taken,
|
||||||
|
created_time: data.created_time,
|
||||||
|
last_modified: data.last_modified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_exif(
|
||||||
|
&mut self,
|
||||||
|
_: &str,
|
||||||
|
) -> Result<Option<crate::database::models::ImageExif>, crate::database::DbError> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_exif(
|
||||||
|
&mut self,
|
||||||
|
data: crate::database::models::InsertImageExif,
|
||||||
|
) -> Result<crate::database::models::ImageExif, crate::database::DbError> {
|
||||||
|
// Return a dummy ImageExif for tests
|
||||||
|
Ok(crate::database::models::ImageExif {
|
||||||
|
id: 1,
|
||||||
|
file_path: data.file_path.to_string(),
|
||||||
|
camera_make: data.camera_make.map(|s| s.to_string()),
|
||||||
|
camera_model: data.camera_model.map(|s| s.to_string()),
|
||||||
|
lens_model: data.lens_model.map(|s| s.to_string()),
|
||||||
|
width: data.width,
|
||||||
|
height: data.height,
|
||||||
|
orientation: data.orientation,
|
||||||
|
gps_latitude: data.gps_latitude,
|
||||||
|
gps_longitude: data.gps_longitude,
|
||||||
|
gps_altitude: data.gps_altitude,
|
||||||
|
focal_length: data.focal_length,
|
||||||
|
aperture: data.aperture,
|
||||||
|
shutter_speed: data.shutter_speed,
|
||||||
|
iso: data.iso,
|
||||||
|
date_taken: data.date_taken,
|
||||||
|
created_time: data.created_time,
|
||||||
|
last_modified: data.last_modified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_exif(&mut self, _: &str) -> Result<(), crate::database::DbError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_all_with_date_taken(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Vec<(String, i64)>, crate::database::DbError> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_exif_batch(
|
||||||
|
&mut self,
|
||||||
|
_: &[String],
|
||||||
|
) -> Result<Vec<crate::database::models::ImageExif>, crate::database::DbError> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_by_exif(
|
||||||
|
&mut self,
|
||||||
|
_: Option<&str>,
|
||||||
|
_: Option<&str>,
|
||||||
|
_: Option<&str>,
|
||||||
|
_: Option<(f64, f64, f64, f64)>,
|
||||||
|
_: Option<i64>,
|
||||||
|
_: Option<i64>,
|
||||||
|
) -> Result<Vec<crate::database::models::ImageExif>, crate::database::DbError> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_camera_makes(&mut self) -> Result<Vec<(String, i64)>, crate::database::DbError> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod api {
|
mod api {
|
||||||
use super::*;
|
use super::*;
|
||||||
use actix_web::{HttpResponse, web::Query};
|
use actix_web::{HttpResponse, web::Query};
|
||||||
@@ -581,6 +888,7 @@ mod tests {
|
|||||||
testhelpers::BodyReader,
|
testhelpers::BodyReader,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::database::SqliteExifDao;
|
||||||
use crate::database::test::in_memory_db_connection;
|
use crate::database::test::in_memory_db_connection;
|
||||||
use crate::tags::SqliteTagDao;
|
use crate::tags::SqliteTagDao;
|
||||||
use actix_web::test::TestRequest;
|
use actix_web::test::TestRequest;
|
||||||
@@ -619,6 +927,9 @@ mod tests {
|
|||||||
Data::new(AppState::test_state()),
|
Data::new(AppState::test_state()),
|
||||||
Data::new(RealFileSystem::new(String::from("/tmp"))),
|
Data::new(RealFileSystem::new(String::from("/tmp"))),
|
||||||
Data::new(Mutex::new(SqliteTagDao::default())),
|
Data::new(Mutex::new(SqliteTagDao::default())),
|
||||||
|
Data::new(Mutex::new(
|
||||||
|
Box::new(MockExifDao) as Box<dyn crate::database::ExifDao>
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
@@ -658,6 +969,9 @@ mod tests {
|
|||||||
Data::new(AppState::test_state()),
|
Data::new(AppState::test_state()),
|
||||||
Data::new(RealFileSystem::new(String::from("./"))),
|
Data::new(RealFileSystem::new(String::from("./"))),
|
||||||
Data::new(Mutex::new(SqliteTagDao::default())),
|
Data::new(Mutex::new(SqliteTagDao::default())),
|
||||||
|
Data::new(Mutex::new(
|
||||||
|
Box::new(MockExifDao) as Box<dyn crate::database::ExifDao>
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -711,6 +1025,9 @@ mod tests {
|
|||||||
Data::new(AppState::test_state()),
|
Data::new(AppState::test_state()),
|
||||||
Data::new(FakeFileSystem::new(files)),
|
Data::new(FakeFileSystem::new(files)),
|
||||||
Data::new(Mutex::new(tag_dao)),
|
Data::new(Mutex::new(tag_dao)),
|
||||||
|
Data::new(Mutex::new(
|
||||||
|
Box::new(MockExifDao) as Box<dyn crate::database::ExifDao>
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -781,6 +1098,9 @@ mod tests {
|
|||||||
Data::new(AppState::test_state()),
|
Data::new(AppState::test_state()),
|
||||||
Data::new(FakeFileSystem::new(files)),
|
Data::new(FakeFileSystem::new(files)),
|
||||||
Data::new(Mutex::new(tag_dao)),
|
Data::new(Mutex::new(tag_dao)),
|
||||||
|
Data::new(Mutex::new(
|
||||||
|
Box::new(MockExifDao) as Box<dyn crate::database::ExifDao>
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
40
src/tags.rs
40
src/tags.rs
@@ -87,6 +87,7 @@ async fn get_tags<D: TagDao>(
|
|||||||
async fn get_all_tags<D: TagDao>(
|
async fn get_all_tags<D: TagDao>(
|
||||||
_: Claims,
|
_: Claims,
|
||||||
tag_dao: web::Data<Mutex<D>>,
|
tag_dao: web::Data<Mutex<D>>,
|
||||||
|
exif_dao: web::Data<Mutex<Box<dyn crate::database::ExifDao>>>,
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
query: web::Query<GetTagsRequest>,
|
query: web::Query<GetTagsRequest>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
@@ -97,17 +98,34 @@ async fn get_all_tags<D: TagDao>(
|
|||||||
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
|
let mut tag_dao = tag_dao.lock().expect("Unable to get TagDao");
|
||||||
tag_dao
|
tag_dao
|
||||||
.get_all_tags(&span_context, query.path.clone())
|
.get_all_tags(&span_context, query.path.clone())
|
||||||
.map(|tags| {
|
.and_then(|tags| {
|
||||||
span_context.span().set_status(Status::Ok);
|
span_context.span().set_status(Status::Ok);
|
||||||
|
|
||||||
HttpResponse::Ok().json(
|
let tags_response = tags
|
||||||
tags.iter()
|
.iter()
|
||||||
.map(|(tag_count, tag)| TagWithTagCount {
|
.map(|(tag_count, tag)| TagWithTagCount {
|
||||||
tag: tag.clone(),
|
tag: tag.clone(),
|
||||||
tag_count: *tag_count,
|
tag_count: *tag_count,
|
||||||
})
|
})
|
||||||
.collect::<Vec<TagWithTagCount>>(),
|
.collect::<Vec<TagWithTagCount>>();
|
||||||
)
|
|
||||||
|
// Get camera makes from EXIF database
|
||||||
|
let camera_makes = exif_dao
|
||||||
|
.lock()
|
||||||
|
.expect("Unable to get ExifDao")
|
||||||
|
.get_camera_makes()
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
log::warn!("Failed to get camera makes: {:?}", e);
|
||||||
|
Vec::new()
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.map(|(make, count)| CameraMakeCount { make, count })
|
||||||
|
.collect::<Vec<CameraMakeCount>>();
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(AllTagsResponse {
|
||||||
|
tags: tags_response,
|
||||||
|
camera_makes,
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
.into_http_internal_err()
|
.into_http_internal_err()
|
||||||
}
|
}
|
||||||
@@ -208,6 +226,18 @@ pub struct TagWithTagCount {
|
|||||||
pub tag: Tag,
|
pub tag: Tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct CameraMakeCount {
|
||||||
|
pub make: String,
|
||||||
|
pub count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
pub struct AllTagsResponse {
|
||||||
|
pub tags: Vec<TagWithTagCount>,
|
||||||
|
pub camera_makes: Vec<CameraMakeCount>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Insertable, Clone, Debug)]
|
#[derive(Insertable, Clone, Debug)]
|
||||||
#[diesel(table_name = tags)]
|
#[diesel(table_name = tags)]
|
||||||
pub struct InsertTag {
|
pub struct InsertTag {
|
||||||
|
|||||||
Reference in New Issue
Block a user