mirror of
https://codeberg.org/Toasterson/ips.git
synced 2026-04-10 13:20:42 +00:00
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:
parent
745d610a0a
commit
d295a6e219
7 changed files with 105 additions and 69 deletions
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
19
pkg6depotd/src/http/handlers/index.rs
Normal file
19
pkg6depotd/src/http/handlers/index.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod catalog;
|
||||
pub mod file;
|
||||
pub mod index;
|
||||
pub mod info;
|
||||
pub mod manifest;
|
||||
pub mod publisher;
|
||||
|
|
|
|||
|
|
@ -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,70 +26,88 @@ 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
|
||||
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..]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue