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))
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh repository metadata
|
/// 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
|
// Filter publishers if specified
|
||||||
let publishers = if let Some(pub_name) = publisher {
|
let publishers = if let Some(pub_name) = publisher {
|
||||||
if !self.config.publishers.contains(&pub_name.to_string()) {
|
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))
|
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(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ rusqlite = { version = "0.31", default-features = false }
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
httpdate = "1"
|
httpdate = "1"
|
||||||
|
urlencoding = "2"
|
||||||
|
|
||||||
# Telemetry
|
# Telemetry
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
@ -51,7 +52,6 @@ miette = { version = "7.6.0", features = ["fancy"] }
|
||||||
|
|
||||||
# Templating & Web UI
|
# Templating & Web UI
|
||||||
askama = "0.15"
|
askama = "0.15"
|
||||||
urlencoding = "2"
|
|
||||||
|
|
||||||
# Project Dependencies
|
# Project Dependencies
|
||||||
libips = { path = "../libips" }
|
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 catalog;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
|
pub mod index;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub mod publisher;
|
pub mod publisher;
|
||||||
|
|
|
||||||
|
|
@ -2,28 +2,21 @@ use crate::errors::DepotError;
|
||||||
use crate::repo::DepotRepo;
|
use crate::repo::DepotRepo;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use libips::repository::IndexEntry;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn get_search_v0(
|
pub async fn get_search_v0(
|
||||||
State(repo): State<Arc<DepotRepo>>,
|
State(repo): State<Arc<DepotRepo>>,
|
||||||
Path((publisher, token)): Path<(String, String)>,
|
Path((publisher, token)): Path<(String, String)>,
|
||||||
) -> Result<Response, DepotError> {
|
) -> 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)?;
|
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();
|
let mut body = String::new();
|
||||||
for entry in results {
|
for entry in results {
|
||||||
body.push_str(&format!(
|
body.push_str(&format!(
|
||||||
"{} {} {} {}\n",
|
"{} {} {} {}\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(
|
pub async fn get_search_v1(
|
||||||
State(repo): State<Arc<DepotRepo>>,
|
State(repo): State<Arc<DepotRepo>>,
|
||||||
Path((publisher, token)): Path<(String, String)>,
|
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> {
|
) -> Result<Response, DepotError> {
|
||||||
// Search v1 token format: "<case>_<rtype>_<trans>_<installroot>_<query>"
|
// Search v1 token format: "<case>_<rtype>_<trans>_<installroot>_<query>"
|
||||||
// Example: "False_2_None_None_%3A%3A%3Apostgres" -> query ":::postgres"
|
// 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)
|
(p, q)
|
||||||
} else {
|
} else {
|
||||||
("False_2_None_None", token.as_str())
|
("False_2_None_None", token)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse prefix fields
|
// Parse prefix fields
|
||||||
let parts: Vec<&str> = prefix.split('_').collect();
|
let parts: Vec<&str> = prefix.split('_').collect();
|
||||||
let case_sensitive = parts.get(0).map(|s| *s == "True").unwrap_or(false);
|
let case_sensitive = parts.first().map(|s| *s == "True").unwrap_or(false);
|
||||||
let p1 = if case_sensitive { "1" } else { "0" }; // query number/flag
|
let return_type = parts.get(1).copied().unwrap_or("2");
|
||||||
let p2 = parts.get(1).copied().unwrap_or("2"); // return type
|
|
||||||
|
|
||||||
// Run search with provided publisher and query
|
// Run search
|
||||||
let results = repo.search(Some(&publisher), query, case_sensitive)?;
|
let results = repo.search(Some(publisher), query, case_sensitive)?;
|
||||||
|
|
||||||
// No results -> 204 No Content per v1 spec
|
// No results -> 204 No Content per v1 spec
|
||||||
if results.is_empty() {
|
if results.is_empty() {
|
||||||
return Ok((axum::http::StatusCode::NO_CONTENT).into_response());
|
return Ok((axum::http::StatusCode::NO_CONTENT).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format: "p1 p2 <fmri> <index_type> <action_type> <value> [k=v ...]"
|
let body = format_v1_results(&results, return_type);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(([(axum::http::header::CONTENT_TYPE, "text/plain")], body).into_response())
|
Ok(([(axum::http::header::CONTENT_TYPE, "text/plain")], body).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_v1_token(token: &str) -> Option<(&str, &str)> {
|
/// Format v1 search results.
|
||||||
// Try to find the 4th underscore
|
///
|
||||||
let mut parts = token.splitn(5, '_');
|
/// return_type "1" -> package-only: `{query_number} {return_type} {fmri}`
|
||||||
if let (Some(_), Some(_), Some(_), Some(_), Some(_)) = (
|
/// return_type "2" (default) -> actions: `{query_number} {return_type} {fmri} {matched_value_urlencoded} {action_line}`
|
||||||
parts.next(),
|
fn format_v1_results(results: &[IndexEntry], return_type: &str) -> String {
|
||||||
parts.next(),
|
let mut body = String::from("Return from search v1\n");
|
||||||
parts.next(),
|
let query_number = 0;
|
||||||
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.
|
|
||||||
|
|
||||||
// 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;
|
let mut underscore_count = 0;
|
||||||
for (i, c) in token.chars().enumerate() {
|
for (i, c) in token.char_indices() {
|
||||||
if c == '_' {
|
if c == '_' {
|
||||||
underscore_count += 1;
|
underscore_count += 1;
|
||||||
if underscore_count == 4 {
|
if underscore_count == 4 {
|
||||||
|
|
@ -99,6 +111,5 @@ fn split_v1_token(token: &str) -> Option<(&str, &str)> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::http::admin;
|
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 crate::repo::DepotRepo;
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
|
|
@ -63,6 +65,11 @@ pub fn app_router(state: Arc<DepotRepo>) -> Router {
|
||||||
.route("/publisher/1/", get(publisher::get_default_publisher_v1))
|
.route("/publisher/1/", get(publisher::get_default_publisher_v1))
|
||||||
.route("/{publisher}/search/0/{token}", get(search::get_search_v0))
|
.route("/{publisher}/search/0/{token}", get(search::get_search_v0))
|
||||||
.route("/{publisher}/search/1/{token}", get(search::get_search_v1))
|
.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
|
// Admin API over HTTP
|
||||||
.route("/admin/health", get(admin::health))
|
.route("/admin/health", get(admin::health))
|
||||||
.route("/admin/auth/check", post(admin::auth_check))
|
.route("/admin/auth/check", post(admin::auth_check))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::errors::{DepotError, Result};
|
use crate::errors::{DepotError, Result};
|
||||||
use libips::fmri::Fmri;
|
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::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
|
@ -100,6 +100,16 @@ impl DepotRepo {
|
||||||
None
|
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 {
|
pub fn cache_max_age(&self) -> u64 {
|
||||||
self.cache_max_age
|
self.cache_max_age
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue