diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs index 1149bcf..7271b4f 100644 --- a/libips/src/repository/file_backend.rs +++ b/libips/src/repository/file_backend.rs @@ -1624,19 +1624,13 @@ impl WritableRepository for FileBackend { RepositoryError::Other(format!("Failed to build catalog shards: {}", e.message)) })?; } - - if !no_index { - // FTS index is now built as part of catalog shards (fts.db) - // No separate index building needed - info!("Search index built as part of catalog shards (fts.db)"); - } } Ok(()) } /// Refresh repository metadata - fn refresh(&self, publisher: Option<&str>, no_catalog: bool, no_index: bool) -> Result<()> { + fn refresh(&self, publisher: Option<&str>, no_catalog: bool, _no_index: bool) -> Result<()> { // Filter publishers if specified let publishers = if let Some(pub_name) = publisher { if !self.config.publishers.contains(&pub_name.to_string()) { @@ -1668,12 +1662,6 @@ impl WritableRepository for FileBackend { RepositoryError::Other(format!("Failed to build catalog shards: {}", e.message)) })?; } - - if !no_index { - // FTS index is now built as part of catalog shards (fts.db) - // No separate index building needed - info!("Search index built as part of catalog shards (fts.db)"); - } } Ok(()) diff --git a/pkg6depotd/Cargo.toml b/pkg6depotd/Cargo.toml index 0e382dc..b5568ca 100644 --- a/pkg6depotd/Cargo.toml +++ b/pkg6depotd/Cargo.toml @@ -36,6 +36,7 @@ rusqlite = { version = "0.31", default-features = false } chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } flate2 = "1" httpdate = "1" +urlencoding = "2" # Telemetry tracing = "0.1" @@ -51,7 +52,6 @@ miette = { version = "7.6.0", features = ["fancy"] } # Templating & Web UI askama = "0.15" -urlencoding = "2" # Project Dependencies libips = { path = "../libips" } diff --git a/pkg6depotd/src/http/handlers/index.rs b/pkg6depotd/src/http/handlers/index.rs new file mode 100644 index 0000000..e4025f0 --- /dev/null +++ b/pkg6depotd/src/http/handlers/index.rs @@ -0,0 +1,19 @@ +use crate::errors::DepotError; +use crate::repo::DepotRepo; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use std::sync::Arc; + +pub async fn get_index_v0( + State(repo): State>, + Path((publisher, command)): Path<(String, String)>, +) -> Result { + match command.as_str() { + "refresh" => { + repo.rebuild_index(Some(&publisher))?; + Ok(StatusCode::OK.into_response()) + } + _ => Ok((StatusCode::BAD_REQUEST, "Unknown command").into_response()), + } +} diff --git a/pkg6depotd/src/http/handlers/mod.rs b/pkg6depotd/src/http/handlers/mod.rs index dd68d98..b707fb8 100644 --- a/pkg6depotd/src/http/handlers/mod.rs +++ b/pkg6depotd/src/http/handlers/mod.rs @@ -1,5 +1,6 @@ pub mod catalog; pub mod file; +pub mod index; pub mod info; pub mod manifest; pub mod publisher; diff --git a/pkg6depotd/src/http/handlers/search.rs b/pkg6depotd/src/http/handlers/search.rs index 98acd16..6b73c9a 100644 --- a/pkg6depotd/src/http/handlers/search.rs +++ b/pkg6depotd/src/http/handlers/search.rs @@ -2,28 +2,21 @@ use crate::errors::DepotError; use crate::repo::DepotRepo; use axum::extract::{Path, State}; use axum::response::{IntoResponse, Response}; +use libips::repository::IndexEntry; use std::sync::Arc; pub async fn get_search_v0( State(repo): State>, Path((publisher, token)): Path<(String, String)>, ) -> Result { - // Decode the token (it might be URL encoded in the path, but axum usually decodes path params? - // Actually, axum decodes percent-encoded path segments automatically if typed as String? - // Let's assume yes or use it as is. - // However, typical search tokens might contain chars that need decoding. - // If standard axum decoding is not enough, we might need manual decoding. - // But let's start with standard. - - // Call search let results = repo.search(Some(&publisher), &token, false)?; - // Format output: index action value package + // Format: {index_type} {fmri} {action_type} {value} let mut body = String::new(); for entry in results { body.push_str(&format!( "{} {} {} {}\n", - entry.index_type, entry.action_type, entry.value, entry.fmri + entry.index_type, entry.fmri, entry.action_type, entry.value )); } @@ -33,70 +26,88 @@ pub async fn get_search_v0( pub async fn get_search_v1( State(repo): State>, Path((publisher, token)): Path<(String, String)>, +) -> Result { + search_v1_impl(&repo, &publisher, &token) +} + +pub async fn post_search_v1( + State(repo): State>, + Path(publisher): Path, + body: String, +) -> Result { + // The POST body contains the token directly + let token = body.trim(); + search_v1_impl(&repo, &publisher, token) +} + +fn search_v1_impl( + repo: &DepotRepo, + publisher: &str, + token: &str, ) -> Result { // Search v1 token format: "____" // Example: "False_2_None_None_%3A%3A%3Apostgres" -> query ":::postgres" - let (prefix, query) = if let Some((p, q)) = split_v1_token(&token) { + let (prefix, query) = if let Some((p, q)) = split_v1_token(token) { (p, q) } else { - ("False_2_None_None", token.as_str()) + ("False_2_None_None", token) }; // Parse prefix fields let parts: Vec<&str> = prefix.split('_').collect(); - let case_sensitive = parts.get(0).map(|s| *s == "True").unwrap_or(false); - let p1 = if case_sensitive { "1" } else { "0" }; // query number/flag - let p2 = parts.get(1).copied().unwrap_or("2"); // return type + let case_sensitive = parts.first().map(|s| *s == "True").unwrap_or(false); + let return_type = parts.get(1).copied().unwrap_or("2"); - // Run search with provided publisher and query - let results = repo.search(Some(&publisher), query, case_sensitive)?; + // Run search + let results = repo.search(Some(publisher), query, case_sensitive)?; // No results -> 204 No Content per v1 spec if results.is_empty() { return Ok((axum::http::StatusCode::NO_CONTENT).into_response()); } - // Format: "p1 p2 [k=v ...]" - let mut body = String::from("Return from search v1\n"); - for entry in results { - let mut line = format!( - "{} {} {} {} {} {}", - p1, p2, entry.fmri, entry.index_type, entry.action_type, entry.value - ); - // Attributes are already in a BTreeMap, so iteration order is stable - for (k, v) in &entry.attributes { - line.push_str(&format!(" {}={}", k, v)); - } - line.push('\n'); - body.push_str(&line); - } - + let body = format_v1_results(&results, return_type); Ok(([(axum::http::header::CONTENT_TYPE, "text/plain")], body).into_response()) } -fn split_v1_token(token: &str) -> Option<(&str, &str)> { - // Try to find the 4th underscore - let mut parts = token.splitn(5, '_'); - if let (Some(_), Some(_), Some(_), Some(_), Some(_)) = ( - parts.next(), - parts.next(), - parts.next(), - parts.next(), - parts.next(), - ) { - // We found 4 parts and a remainder. - // We need to reconstruct where the split happened to return slices - // Actually, splitn(5) returns 5 parts. The last part is the remainder. - // But we want to be careful about the length of the prefix. +/// Format v1 search results. +/// +/// return_type "1" -> package-only: `{query_number} {return_type} {fmri}` +/// return_type "2" (default) -> actions: `{query_number} {return_type} {fmri} {matched_value_urlencoded} {action_line}` +fn format_v1_results(results: &[IndexEntry], return_type: &str) -> String { + let mut body = String::from("Return from search v1\n"); + let query_number = 0; - // Let's iterate chars to find 4th underscore - let mut underscore_count = 0; - for (i, c) in token.chars().enumerate() { - if c == '_' { - underscore_count += 1; - if underscore_count == 4 { - return Some((&token[..i], &token[i + 1..])); + for entry in results { + match return_type { + "1" => { + body.push_str(&format!("{} {} {}\n", query_number, return_type, entry.fmri)); + } + _ => { + let matched_value = urlencoding::encode(&entry.value); + let mut action_line = entry.action_type.clone(); + for (k, v) in &entry.attributes { + action_line.push_str(&format!(" {}={}", k, v)); } + body.push_str(&format!( + "{} {} {} {} {}\n", + query_number, return_type, entry.fmri, matched_value, action_line + )); + } + } + } + + body +} + +fn split_v1_token(token: &str) -> Option<(&str, &str)> { + // Find the 4th underscore to split prefix from query + let mut underscore_count = 0; + for (i, c) in token.char_indices() { + if c == '_' { + underscore_count += 1; + if underscore_count == 4 { + return Some((&token[..i], &token[i + 1..])); } } } diff --git a/pkg6depotd/src/http/routes.rs b/pkg6depotd/src/http/routes.rs index e933b1c..42f4225 100644 --- a/pkg6depotd/src/http/routes.rs +++ b/pkg6depotd/src/http/routes.rs @@ -1,5 +1,7 @@ use crate::http::admin; -use crate::http::handlers::{catalog, file, info, manifest, publisher, search, shard, ui, versions}; +use crate::http::handlers::{ + catalog, file, index, info, manifest, publisher, search, shard, ui, versions, +}; use crate::repo::DepotRepo; use axum::{ Router, @@ -63,6 +65,11 @@ pub fn app_router(state: Arc) -> Router { .route("/publisher/1/", get(publisher::get_default_publisher_v1)) .route("/{publisher}/search/0/{token}", get(search::get_search_v0)) .route("/{publisher}/search/1/{token}", get(search::get_search_v1)) + .route("/{publisher}/search/1/", post(search::post_search_v1)) + .route( + "/{publisher}/index/0/{command}", + get(index::get_index_v0), + ) // Admin API over HTTP .route("/admin/health", get(admin::health)) .route("/admin/auth/check", post(admin::auth_check)) diff --git a/pkg6depotd/src/repo.rs b/pkg6depotd/src/repo.rs index 76e10e3..e71c195 100644 --- a/pkg6depotd/src/repo.rs +++ b/pkg6depotd/src/repo.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::errors::{DepotError, Result}; use libips::fmri::Fmri; -use libips::repository::{FileBackend, IndexEntry, PackageInfo, ReadableRepository}; +use libips::repository::{FileBackend, IndexEntry, PackageInfo, ReadableRepository, WritableRepository}; use std::path::PathBuf; use std::sync::Mutex; @@ -100,6 +100,16 @@ impl DepotRepo { None } + pub fn rebuild_index(&self, publisher: Option<&str>) -> Result<()> { + let backend = self + .backend + .lock() + .map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?; + backend + .rebuild(publisher, true, false) + .map_err(DepotError::Repo) + } + pub fn cache_max_age(&self) -> u64 { self.cache_max_age }