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:
@@ -5,8 +5,10 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ai::{InsightGenerator, ModelCapabilities, OllamaClient};
|
||||
use crate::data::Claims;
|
||||
use crate::database::InsightDao;
|
||||
use crate::database::{ExifDao, InsightDao};
|
||||
use crate::libraries;
|
||||
use crate::otel::{extract_context_from_request, global_tracer};
|
||||
use crate::state::AppState;
|
||||
use crate::utils::normalize_path;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -31,6 +33,10 @@ pub struct GeneratePhotoInsightRequest {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetPhotoInsightQuery {
|
||||
pub path: String,
|
||||
/// Library context for this lookup. Used to pick the right content
|
||||
/// hash when the same rel_path exists under multiple roots.
|
||||
#[serde(default)]
|
||||
pub library: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -146,15 +152,30 @@ pub async fn generate_insight_handler(
|
||||
pub async fn get_insight_handler(
|
||||
_claims: Claims,
|
||||
query: web::Query<GetPhotoInsightQuery>,
|
||||
app_state: web::Data<AppState>,
|
||||
insight_dao: web::Data<std::sync::Mutex<Box<dyn InsightDao>>>,
|
||||
exif_dao: web::Data<std::sync::Mutex<Box<dyn ExifDao>>>,
|
||||
) -> impl Responder {
|
||||
let normalized_path = normalize_path(&query.path);
|
||||
log::debug!("Fetching insight for {}", normalized_path);
|
||||
|
||||
let otel_context = opentelemetry::Context::new();
|
||||
|
||||
// Expand to rel_paths sharing content so an insight generated under
|
||||
// library 1 still shows when the same photo is viewed from library 2.
|
||||
let library = libraries::resolve_library_param(&app_state, query.library.as_deref())
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| app_state.primary_library());
|
||||
let sibling_paths = {
|
||||
let mut exif = exif_dao.lock().expect("Unable to lock ExifDao");
|
||||
exif.get_rel_paths_sharing_content(&otel_context, library.id, &normalized_path)
|
||||
.unwrap_or_else(|_| vec![normalized_path.clone()])
|
||||
};
|
||||
|
||||
let mut dao = insight_dao.lock().expect("Unable to lock InsightDao");
|
||||
|
||||
match dao.get_insight(&otel_context, &normalized_path) {
|
||||
match dao.get_insight_for_paths(&otel_context, &sibling_paths) {
|
||||
Ok(Some(insight)) => {
|
||||
let response = PhotoInsightResponse {
|
||||
id: insight.id,
|
||||
|
||||
Reference in New Issue
Block a user