feat: content-hash-aware tag/insight sharing + library scoping
Tags and insights now follow content across libraries via content_hash lookups on the read path, so the same file indexed at different rel_paths in multiple libraries shares its annotations. Recursive tag search scopes hits to the selected library by checking each tagged rel_path against the library's disk (with a content-hash sibling fallback so tags attached under one library's rel_path still match a content-equivalent file in another). The /image and /image/metadata handlers fall back across libraries when the file isn't under the resolved one, so union-mode search results (which carry no library attribution in the response) still serve correctly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
44
src/main.rs
44
src/main.rs
@@ -118,7 +118,25 @@ async fn get_image(
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(path) = is_valid_full_path(&library.root_path, &req.path, false) {
|
||||
// Union-mode search returns flat rel_paths with no library attribution,
|
||||
// so clients may request a file under the wrong library. Try the
|
||||
// resolved library first; if the file isn't there, fall back to any
|
||||
// other library holding that rel_path on disk.
|
||||
let resolved = is_valid_full_path(&library.root_path, &req.path, false)
|
||||
.filter(|p| p.exists())
|
||||
.map(|p| (library, p))
|
||||
.or_else(|| {
|
||||
app_state.libraries.iter().find_map(|lib| {
|
||||
if lib.id == library.id {
|
||||
return None;
|
||||
}
|
||||
is_valid_full_path(&lib.root_path, &req.path, false)
|
||||
.filter(|p| p.exists())
|
||||
.map(|p| (lib, p))
|
||||
})
|
||||
});
|
||||
|
||||
if let Some((library, path)) = resolved {
|
||||
let image_size = req.size.unwrap_or(PhotoSize::Full);
|
||||
if image_size == PhotoSize::Thumb {
|
||||
let relative_path = path
|
||||
@@ -207,9 +225,9 @@ async fn get_image(
|
||||
span.set_status(Status::error("Not found"));
|
||||
HttpResponse::NotFound().finish()
|
||||
} else {
|
||||
span.set_status(Status::error("Bad photos request"));
|
||||
error!("Bad photos request: {}", req.path);
|
||||
HttpResponse::BadRequest().finish()
|
||||
span.set_status(Status::error("Not found"));
|
||||
error!("Path does not exist in any library: {}", req.path);
|
||||
HttpResponse::NotFound().finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +312,23 @@ async fn get_file_metadata(
|
||||
let span_context =
|
||||
opentelemetry::Context::new().with_remote_span_context(span.span_context().clone());
|
||||
|
||||
let full_path = is_valid_full_path(&app_state.base_path, &path.path, false);
|
||||
let library = libraries::resolve_library_param(&app_state, path.library.as_deref())
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| app_state.primary_library());
|
||||
|
||||
// Fall back to other libraries if the file isn't under the resolved one,
|
||||
// matching the `/image` handler so union-mode search results resolve.
|
||||
let full_path = is_valid_full_path(&library.root_path, &path.path, false)
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| {
|
||||
app_state.libraries.iter().find_map(|lib| {
|
||||
if lib.id == library.id {
|
||||
return None;
|
||||
}
|
||||
is_valid_full_path(&lib.root_path, &path.path, false).filter(|p| p.exists())
|
||||
})
|
||||
});
|
||||
|
||||
match full_path
|
||||
.ok_or_else(|| ErrorKind::InvalidData.into())
|
||||
|
||||
Reference in New Issue
Block a user