fix: Resolve merge conflict between UI and search compatibility branches

Merge both the UI handler (index, ui modules) and the legacy pkg5
search endpoint changes. Remove the obsolete JSON-based
build_search_index method since search now uses FTS5 via
sqlite_catalog::build_shards.
This commit is contained in:
Till Wegmueller 2026-03-23 17:27:36 +01:00
parent 745d610a0a
commit d295a6e219
No known key found for this signature in database
7 changed files with 105 additions and 69 deletions

View file

@ -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(())

View file

@ -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" }

View file

@ -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<Arc<DepotRepo>>,
Path((publisher, command)): Path<(String, String)>,
) -> Result<Response, DepotError> {
match command.as_str() {
"refresh" => {
repo.rebuild_index(Some(&publisher))?;
Ok(StatusCode::OK.into_response())
}
_ => Ok((StatusCode::BAD_REQUEST, "Unknown command").into_response()),
}
}

View file

@ -1,5 +1,6 @@
pub mod catalog;
pub mod file;
pub mod index;
pub mod info;
pub mod manifest;
pub mod publisher;

View file

@ -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<Arc<DepotRepo>>,
Path((publisher, token)): Path<(String, String)>,
) -> Result<Response, DepotError> {
// 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,65 +26,84 @@ pub async fn get_search_v0(
pub async fn get_search_v1(
State(repo): State<Arc<DepotRepo>>,
Path((publisher, token)): Path<(String, String)>,
) -> Result<Response, DepotError> {
search_v1_impl(&repo, &publisher, &token)
}
pub async fn post_search_v1(
State(repo): State<Arc<DepotRepo>>,
Path(publisher): Path<String>,
body: String,
) -> Result<Response, DepotError> {
// 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<Response, DepotError> {
// Search v1 token format: "<case>_<rtype>_<trans>_<installroot>_<query>"
// 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 <fmri> <index_type> <action_type> <value> [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
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.chars().enumerate() {
for (i, c) in token.char_indices() {
if c == '_' {
underscore_count += 1;
if underscore_count == 4 {
@ -99,6 +111,5 @@ fn split_v1_token(token: &str) -> Option<(&str, &str)> {
}
}
}
}
None
}

View file

@ -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<DepotRepo>) -> 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))

View file

@ -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
}