use axum::{
extract::{Path, State},
http::StatusCode,
response::{IntoResponse, Json, Response},
};
use serde::Deserialize;
use std::sync::Arc;
use crate::core::registry::{IndexHandle, IndexId};
use super::helpers::{embedder_error_response, embedder_initializing_response, validate_root_path};
use super::state::{DaemonEvent, SearchAppState};
#[derive(Deserialize)]
pub(super) struct RelocateIndexRequest {
pub root_path: std::path::PathBuf,
}
pub(super) async fn relocate_index_handler(
State(state): State<Arc<SearchAppState>>,
Path(id): Path<String>,
Json(req): Json<RelocateIndexRequest>,
) -> Response {
let index_id = IndexId::new(id.clone());
let existing = match state.registry.get(&index_id) {
Some(h) => h,
None => {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": format!("unknown index: {id}") })),
)
.into_response();
}
};
let new_root = match validate_root_path(&req.root_path).await {
Ok(p) => p,
Err(resp) => return resp,
};
let Some(embedder) = state.current_embedder().await else {
if let Some(err) = state.current_embedder_error() {
return embedder_error_response(&err);
}
return embedder_initializing_response();
};
let on_disk = crate::service::persistence::load_index_registry()
.ok()
.and_then(|entries| entries.into_iter().find(|e| e.id == id));
let on_disk_colocated = on_disk.as_ref().map(|e| e.colocated).unwrap_or(false);
let on_disk_last_queried = on_disk.as_ref().and_then(|e| e.last_queried_unix);
let on_disk_last_indexed = on_disk.as_ref().and_then(|e| e.last_indexed_unix);
let existing_entry = crate::service::persistence::PersistedIndex {
id: id.clone(),
root_path: new_root.clone(),
include_paths: existing
.include_paths
.iter()
.filter_map(|p| p.to_str().map(str::to_string))
.collect(),
exclude_globs: existing.exclude_globs.clone(),
extensions: existing.extensions.clone(),
domain_terms: existing.domain_terms.clone(),
path_filter: existing.path_filter.clone(),
include_docs: existing.include_docs,
respect_gitignore: existing.respect_gitignore,
extra_skip_dirs: existing.extra_skip_dirs.clone(),
data_file_max_bytes: Some(existing.data_file_max_bytes),
lexical_only: existing.lexical_only,
skip_kg: existing.skip_kg,
defer_embed: existing.defer_embed,
colocated: on_disk_colocated,
last_queried_unix: on_disk_last_queried,
last_indexed_unix: on_disk_last_indexed,
};
let new_indexer = match crate::service::persistence_loader::build_indexer_from_entry(
&existing_entry,
&embedder,
)
.await
{
Ok(idx) => idx,
Err(e) => {
tracing::error!(
"relocate[{id}]: failed to rebuild indexer at {}: {e}",
new_root.display()
);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": format!("indexer rebuild failed: {e}") })),
)
.into_response();
}
};
if let Err(e) = crate::service::persistence::upsert_index_registry_entry(existing_entry.clone())
{
tracing::warn!("relocate[{id}]: could not persist new root_path to indexes.toml: {e}");
}
if let Err(e) = crate::service::roots_registry::upsert_root(new_root.clone()) {
tracing::warn!("relocate[{id}]: could not update roots.toml: {e}");
}
let new_handle = IndexHandle {
id: index_id.clone(),
indexer: Arc::new(tokio::sync::RwLock::new(new_indexer)),
root_path: new_root.clone(),
include_paths: existing.include_paths.clone(),
exclude_globs: existing.exclude_globs.clone(),
extensions: existing.extensions.clone(),
domain_terms: existing.domain_terms.clone(),
include_docs: existing.include_docs,
respect_gitignore: existing.respect_gitignore,
extra_skip_dirs: existing.extra_skip_dirs.clone(),
data_file_max_bytes: existing.data_file_max_bytes,
path_filter: existing.path_filter.clone(),
context_embedding: Arc::clone(&existing.context_embedding),
context_summary: Arc::clone(&existing.context_summary),
indexed_head_sha: Arc::clone(&existing.indexed_head_sha),
last_indexed_at: Arc::clone(&existing.last_indexed_at),
lexical_only: existing.lexical_only,
skip_kg: existing.skip_kg,
defer_embed: existing.defer_embed,
stages: Arc::clone(&existing.stages),
search_pressure: Arc::clone(&existing.search_pressure),
walk_diagnostics: Arc::clone(&existing.walk_diagnostics),
};
state.registry.register(new_handle);
if let Some(h) = state.registry.get(&index_id) {
if let Err(e) = h.write_indexed_root(&new_root).await {
tracing::warn!(
"relocate[{id}]: failed to update indexed_root in corpus \
(next reindex may re-detect root move): {e}"
);
}
}
state.emit(DaemonEvent::IndexRegistered { id: id.clone() });
tracing::info!("relocate[{id}]: rebind complete → {}", new_root.display());
Json(serde_json::json!({
"id": id,
"relocated": true,
"new_root_path": new_root.to_string_lossy(),
}))
.into_response()
}