fix: validate gps-summary path against every library

The /photos/gps-summary handler validated the incoming path against
the primary library's root with new_file=false, which requires the
path to exist on disk. For a viewer opened on a file from a
non-primary library, tapping the GPS link produced activePath =
<folder from lib 2>, the primary-only check failed, and the server
400'd — so the map came up empty.

Validation here is purely a traversal guard (the DAO does a prefix
LIKE against rel_path), so we now accept the path as long as any
configured library can resolve it without escaping its root.

Also applies cargo fmt drift on files touched this session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cameron
2026-04-18 16:38:28 -04:00
committed by cameron
parent 54a1df60b8
commit a0f3bfab5f
4 changed files with 55 additions and 59 deletions

View File

@@ -55,13 +55,13 @@ use opentelemetry::{KeyValue, global};
mod ai;
mod auth;
mod content_hash;
mod data;
mod database;
mod error;
mod exif;
mod file_types;
mod files;
mod content_hash;
mod geo;
mod libraries;
mod state;
@@ -106,10 +106,7 @@ async fn get_image(
// Resolve library from query param; default to primary so clients that
// don't yet send `library=` continue to work.
let library = match libraries::resolve_library_param(
&app_state,
req.library.as_deref(),
) {
let library = match libraries::resolve_library_param(&app_state, req.library.as_deref()) {
Ok(Some(lib)) => lib,
Ok(None) => app_state.primary_library(),
Err(msg) => {
@@ -339,8 +336,7 @@ async fn get_file_metadata(
File::open(&full_path)
.and_then(|file| file.metadata())
.map(|metadata| (lib, metadata))
})
{
}) {
Ok((resolved_library, metadata)) => {
let mut response: MetadataResponse = metadata.into();
response.library_id = Some(resolved_library.id);
@@ -397,17 +393,15 @@ async fn upload_image(
// Resolve the optional library selector. Absent → primary library
// (backwards-compatible with clients that don't yet send `library=`).
let target_library = match libraries::resolve_library_param(
&app_state,
query.library.as_deref(),
) {
Ok(Some(lib)) => lib,
Ok(None) => app_state.primary_library(),
Err(msg) => {
span.set_status(Status::error(msg.clone()));
return HttpResponse::BadRequest().body(msg);
}
};
let target_library =
match libraries::resolve_library_param(&app_state, query.library.as_deref()) {
Ok(Some(lib)) => lib,
Ok(None) => app_state.primary_library(),
Err(msg) => {
span.set_status(Status::error(msg.clone()));
return HttpResponse::BadRequest().body(msg);
}
};
let mut file_content: BytesMut = BytesMut::new();
let mut file_name: Option<String> = None;
@@ -496,18 +490,18 @@ async fn upload_image(
match exif::extract_exif_from_path(&uploaded_path) {
Ok(exif_data) => {
let timestamp = Utc::now().timestamp();
let (content_hash, size_bytes) =
match content_hash::compute(&uploaded_path) {
Ok(id) => (Some(id.content_hash), Some(id.size_bytes)),
Err(e) => {
warn!(
"Failed to hash uploaded {}: {:?}",
uploaded_path.display(),
e
);
(None, None)
}
};
let (content_hash, size_bytes) = match content_hash::compute(&uploaded_path)
{
Ok(id) => (Some(id.content_hash), Some(id.size_bytes)),
Err(e) => {
warn!(
"Failed to hash uploaded {}: {:?}",
uploaded_path.display(),
e
);
(None, None)
}
};
let insert_exif = InsertImageExif {
library_id: target_library.id,
file_path: relative_path.clone(),
@@ -1827,7 +1821,10 @@ fn process_new_files(
let mut dao = exif_dao.lock().expect("Unable to lock ExifDao");
if let Err(e) = dao.store_exif(&context, insert_exif) {
error!("Failed to register {} in image_exif: {:?}", relative_path, e);
error!(
"Failed to register {} in image_exif: {:?}",
relative_path, e
);
} else {
debug!("Registered {} in image_exif", relative_path);
}