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:
31
src/files.rs
31
src/files.rs
@@ -241,10 +241,8 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
||||
// Resolve the optional library filter. Unknown values return 400.
|
||||
// For Phase 3 the filesystem walk still operates against a single
|
||||
// library's root; Phase 4 introduces multi-root union scanning.
|
||||
let library = match crate::libraries::resolve_library_param(
|
||||
&app_state,
|
||||
req.library.as_deref(),
|
||||
) {
|
||||
let library = match crate::libraries::resolve_library_param(&app_state, req.library.as_deref())
|
||||
{
|
||||
Ok(lib) => lib,
|
||||
Err(msg) => {
|
||||
log::warn!("Rejecting /photos request: {}", msg);
|
||||
@@ -560,8 +558,9 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
||||
.fold((Vec::new(), Vec::new()), |(mut files, mut dirs), path| {
|
||||
match path.metadata() {
|
||||
Ok(md) => {
|
||||
let relative =
|
||||
path.strip_prefix(&scoped_library.root_path).unwrap_or_else(|_| {
|
||||
let relative = path
|
||||
.strip_prefix(&scoped_library.root_path)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Unable to strip library root {} from file path {}",
|
||||
&scoped_library.root_path,
|
||||
@@ -572,10 +571,7 @@ pub async fn list_photos<TagD: TagDao, FS: FileSystemAccess>(
|
||||
// lookups (tags, EXIF, insights) that store
|
||||
// rel_paths with forward slashes still match
|
||||
// on Windows.
|
||||
let relative_str = relative
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.replace('\\', "/");
|
||||
let relative_str = relative.to_str().unwrap().replace('\\', "/");
|
||||
|
||||
if md.is_file() {
|
||||
files.push(relative_str);
|
||||
@@ -1024,13 +1020,20 @@ pub async fn get_gps_summary(
|
||||
|
||||
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
|
||||
// The database stores relative paths, so we use the path as-is.
|
||||
// Normalize empty path or "/" to return all GPS photos. Validation
|
||||
// is purely a traversal guard — the path need not exist on disk
|
||||
// under any particular library, because the DAO just does a prefix
|
||||
// match against image_exif.rel_path (which is library-agnostic for
|
||||
// this summary query).
|
||||
let requested_path = if req.path.is_empty() || req.path == "/" {
|
||||
String::new()
|
||||
} else {
|
||||
// Validate path using the same check as all other endpoints
|
||||
if is_valid_full_path(&app_state.base_path, &req.path, false).is_none() {
|
||||
let req_path = PathBuf::from(&req.path);
|
||||
let validated = app_state.libraries.iter().any(|lib| {
|
||||
is_valid_full_path(&PathBuf::from(&lib.root_path), &req_path, true).is_some()
|
||||
});
|
||||
if !validated {
|
||||
warn!("Invalid path for GPS summary: {}", req.path);
|
||||
cx.span().set_status(Status::error("Invalid path"));
|
||||
return Ok(HttpResponse::BadRequest().json(serde_json::json!({
|
||||
|
||||
Reference in New Issue
Block a user