2025-08-04 22:01:38 +02:00
|
|
|
use crate::actions::{Manifest};
|
|
|
|
|
use crate::fmri::Fmri;
|
|
|
|
|
use crate::repository::catalog::{CatalogManager, CatalogPart, PackageVersionEntry};
|
|
|
|
|
use miette::Diagnostic;
|
2025-08-19 14:30:55 +02:00
|
|
|
use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition};
|
2025-08-04 22:01:38 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use thiserror::Error;
|
2025-08-13 19:09:22 +02:00
|
|
|
use tracing::{info, warn, trace};
|
2025-08-19 22:43:50 +02:00
|
|
|
use std::io::{Cursor, Read, Write};
|
|
|
|
|
use lz4::{Decoder as Lz4Decoder, EncoderBuilder as Lz4EncoderBuilder};
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
/// Table definition for the catalog database
|
|
|
|
|
/// Key: stem@version
|
|
|
|
|
/// Value: serialized Manifest
|
|
|
|
|
pub const CATALOG_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("catalog");
|
|
|
|
|
|
|
|
|
|
/// Table definition for the obsoleted packages catalog
|
|
|
|
|
/// Key: full FMRI including publisher (pkg://publisher/stem@version)
|
|
|
|
|
/// Value: nothing
|
|
|
|
|
pub const OBSOLETED_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("obsoleted");
|
|
|
|
|
|
2025-08-19 14:30:55 +02:00
|
|
|
/// Table definition for the incorporate locks table
|
|
|
|
|
/// Key: stem (e.g., "compress/gzip")
|
|
|
|
|
/// Value: version string as bytes (same format as Fmri::version())
|
|
|
|
|
pub const INCORPORATE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("incorporate");
|
|
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
/// Errors that can occur when working with the image catalog
|
|
|
|
|
#[derive(Error, Debug, Diagnostic)]
|
|
|
|
|
pub enum CatalogError {
|
|
|
|
|
#[error("IO error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::io))]
|
|
|
|
|
IO(#[from] std::io::Error),
|
|
|
|
|
|
|
|
|
|
#[error("JSON error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::json))]
|
|
|
|
|
Json(#[from] serde_json::Error),
|
|
|
|
|
|
|
|
|
|
#[error("Database error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::database))]
|
|
|
|
|
Database(String),
|
|
|
|
|
|
|
|
|
|
#[error("Repository error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::repository))]
|
|
|
|
|
Repository(#[from] crate::repository::RepositoryError),
|
|
|
|
|
|
|
|
|
|
#[error("Action error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::action))]
|
|
|
|
|
Action(#[from] crate::actions::ActionError),
|
|
|
|
|
|
|
|
|
|
#[error("Publisher not found: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::publisher_not_found))]
|
|
|
|
|
PublisherNotFound(String),
|
|
|
|
|
|
|
|
|
|
#[error("No publishers configured")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::no_publishers))]
|
|
|
|
|
NoPublishers,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Result type for catalog operations
|
|
|
|
|
pub type Result<T> = std::result::Result<T, CatalogError>;
|
|
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Internal helpers for (de)compressing manifest JSON payloads stored in redb
|
|
|
|
|
fn is_likely_json(bytes: &[u8]) -> bool {
|
|
|
|
|
let mut i = 0;
|
|
|
|
|
while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') { i += 1; }
|
|
|
|
|
if i >= bytes.len() { return false; }
|
|
|
|
|
matches!(bytes[i], b'{' | b'[')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compress_json_lz4(bytes: &[u8]) -> Result<Vec<u8>> {
|
|
|
|
|
let mut dst = Vec::with_capacity(bytes.len() / 2 + 32);
|
|
|
|
|
let mut enc = Lz4EncoderBuilder::new()
|
|
|
|
|
.level(4)
|
|
|
|
|
.build(Cursor::new(&mut dst))
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create LZ4 encoder: {}", e)))?;
|
|
|
|
|
enc.write_all(bytes)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to write to LZ4 encoder: {}", e)))?;
|
|
|
|
|
let (_out, res) = enc.finish();
|
|
|
|
|
res.map_err(|e| CatalogError::Database(format!("Failed to finish LZ4 encoding: {}", e)))?;
|
|
|
|
|
Ok(dst)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn decode_manifest_bytes(bytes: &[u8]) -> Result<Manifest> {
|
|
|
|
|
// Fast path: uncompressed legacy JSON
|
|
|
|
|
if is_likely_json(bytes) {
|
|
|
|
|
return Ok(serde_json::from_slice::<Manifest>(bytes)?);
|
|
|
|
|
}
|
|
|
|
|
// Try LZ4 frame decode
|
|
|
|
|
let mut decoder = match Lz4Decoder::new(Cursor::new(bytes)) {
|
|
|
|
|
Ok(d) => d,
|
|
|
|
|
Err(_) => {
|
|
|
|
|
// Fallback: attempt JSON anyway
|
|
|
|
|
return Ok(serde_json::from_slice::<Manifest>(bytes)?);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let mut out = Vec::new();
|
|
|
|
|
if let Err(_e) = decoder.read_to_end(&mut out) {
|
|
|
|
|
// On decode failure, try JSON as last resort
|
|
|
|
|
return Ok(serde_json::from_slice::<Manifest>(bytes)?);
|
|
|
|
|
}
|
|
|
|
|
Ok(serde_json::from_slice::<Manifest>(&out)?)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
/// Information about a package in the catalog
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PackageInfo {
|
|
|
|
|
/// The FMRI of the package
|
|
|
|
|
pub fmri: Fmri,
|
|
|
|
|
|
|
|
|
|
/// Whether the package is obsolete
|
|
|
|
|
pub obsolete: bool,
|
|
|
|
|
|
|
|
|
|
/// The publisher of the package
|
|
|
|
|
pub publisher: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The image catalog, which merges catalogs from all publishers
|
|
|
|
|
pub struct ImageCatalog {
|
2025-08-19 22:43:50 +02:00
|
|
|
/// Path to the catalog database (non-obsolete manifests)
|
2025-08-04 22:01:38 +02:00
|
|
|
db_path: PathBuf,
|
2025-08-19 22:43:50 +02:00
|
|
|
/// Path to the separate obsoleted database
|
|
|
|
|
obsoleted_db_path: PathBuf,
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
/// Path to the catalog directory
|
|
|
|
|
catalog_dir: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ImageCatalog {
|
|
|
|
|
/// Create a new image catalog
|
2025-08-19 22:43:50 +02:00
|
|
|
pub fn new<P: AsRef<Path>>(catalog_dir: P, db_path: P, obsoleted_db_path: P) -> Self {
|
2025-08-04 22:01:38 +02:00
|
|
|
ImageCatalog {
|
|
|
|
|
db_path: db_path.as_ref().to_path_buf(),
|
2025-08-19 22:43:50 +02:00
|
|
|
obsoleted_db_path: obsoleted_db_path.as_ref().to_path_buf(),
|
2025-08-04 22:01:38 +02:00
|
|
|
catalog_dir: catalog_dir.as_ref().to_path_buf(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 00:20:57 +02:00
|
|
|
/// Dump the contents of a specific table to stdout for debugging
|
|
|
|
|
pub fn dump_table(&self, table_name: &str) -> Result<()> {
|
2025-08-19 22:43:50 +02:00
|
|
|
// Determine which table to dump and open the appropriate database
|
2025-08-05 00:20:57 +02:00
|
|
|
match table_name {
|
2025-08-19 22:43:50 +02:00
|
|
|
"catalog" => {
|
|
|
|
|
let db = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
|
|
|
|
let tx = db.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
self.dump_catalog_table(&tx)?;
|
|
|
|
|
}
|
|
|
|
|
"obsoleted" => {
|
|
|
|
|
let db = Database::open(&self.obsoleted_db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
|
|
|
|
let tx = db.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
self.dump_obsoleted_table(&tx)?;
|
|
|
|
|
}
|
2025-08-19 14:30:55 +02:00
|
|
|
"incorporate" => {
|
2025-08-19 22:43:50 +02:00
|
|
|
let db = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
|
|
|
|
let tx = db.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
2025-08-19 14:30:55 +02:00
|
|
|
// Simple dump of incorporate locks
|
|
|
|
|
if let Ok(table) = tx.open_table(INCORPORATE_TABLE) {
|
|
|
|
|
for entry in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate incorporate table: {}", e)))? {
|
|
|
|
|
let (k, v) = entry.map_err(|e| CatalogError::Database(format!("Failed to read incorporate table entry: {}", e)))?;
|
|
|
|
|
let stem = k.value();
|
|
|
|
|
let ver = String::from_utf8_lossy(v.value());
|
|
|
|
|
println!("{} -> {}", stem, ver);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-05 00:20:57 +02:00
|
|
|
_ => return Err(CatalogError::Database(format!("Unknown table: {}", table_name))),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Dump the contents of all tables to stdout for debugging
|
|
|
|
|
pub fn dump_all_tables(&self) -> Result<()> {
|
2025-08-19 22:43:50 +02:00
|
|
|
// Catalog DB
|
|
|
|
|
let db_cat = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
|
|
|
|
let tx_cat = db_cat.begin_read()
|
2025-08-05 00:20:57 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
println!("=== CATALOG TABLE ===");
|
2025-08-19 22:43:50 +02:00
|
|
|
let _ = self.dump_catalog_table(&tx_cat);
|
2025-08-05 00:20:57 +02:00
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Obsoleted DB
|
|
|
|
|
let db_obs = Database::open(&self.obsoleted_db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
|
|
|
|
let tx_obs = db_obs.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
2025-08-05 00:20:57 +02:00
|
|
|
println!("\n=== OBSOLETED TABLE ===");
|
2025-08-19 22:43:50 +02:00
|
|
|
let _ = self.dump_obsoleted_table(&tx_obs);
|
2025-08-05 00:20:57 +02:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Dump the contents of the catalog table
|
|
|
|
|
fn dump_catalog_table(&self, tx: &redb::ReadTransaction) -> Result<()> {
|
|
|
|
|
match tx.open_table(CATALOG_TABLE) {
|
|
|
|
|
Ok(table) => {
|
|
|
|
|
let mut count = 0;
|
|
|
|
|
for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? {
|
|
|
|
|
let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?;
|
|
|
|
|
let key_str = key.value();
|
|
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Try to deserialize the manifest (supports JSON or LZ4-compressed JSON)
|
|
|
|
|
match decode_manifest_bytes(value.value()) {
|
2025-08-05 00:20:57 +02:00
|
|
|
Ok(manifest) => {
|
|
|
|
|
// Extract the publisher from the FMRI attribute
|
|
|
|
|
let publisher = manifest.attributes.iter()
|
|
|
|
|
.find(|attr| attr.key == "pkg.fmri")
|
|
|
|
|
.and_then(|attr| attr.values.get(0).cloned())
|
|
|
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
|
|
|
|
|
|
|
|
println!("Key: {}", key_str);
|
|
|
|
|
println!(" FMRI: {}", publisher);
|
|
|
|
|
println!(" Attributes: {}", manifest.attributes.len());
|
|
|
|
|
println!(" Files: {}", manifest.files.len());
|
|
|
|
|
println!(" Directories: {}", manifest.directories.len());
|
|
|
|
|
println!(" Dependencies: {}", manifest.dependencies.len());
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("Key: {}", key_str);
|
|
|
|
|
println!(" Error deserializing manifest: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
count += 1;
|
|
|
|
|
}
|
|
|
|
|
println!("Total entries in catalog table: {}", count);
|
|
|
|
|
Ok(())
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("Error opening catalog table: {}", e);
|
|
|
|
|
Err(CatalogError::Database(format!("Failed to open catalog table: {}", e)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Dump the contents of the obsoleted table
|
|
|
|
|
fn dump_obsoleted_table(&self, tx: &redb::ReadTransaction) -> Result<()> {
|
|
|
|
|
match tx.open_table(OBSOLETED_TABLE) {
|
|
|
|
|
Ok(table) => {
|
|
|
|
|
let mut count = 0;
|
|
|
|
|
for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? {
|
|
|
|
|
let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?;
|
|
|
|
|
let key_str = key.value();
|
|
|
|
|
|
|
|
|
|
println!("Key: {}", key_str);
|
|
|
|
|
count += 1;
|
|
|
|
|
}
|
|
|
|
|
println!("Total entries in obsoleted table: {}", count);
|
|
|
|
|
Ok(())
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("Error opening obsoleted table: {}", e);
|
|
|
|
|
Err(CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Get database statistics
|
|
|
|
|
pub fn get_db_stats(&self) -> Result<()> {
|
2025-08-19 22:43:50 +02:00
|
|
|
// Open the catalog database
|
|
|
|
|
let db_cat = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
|
|
|
|
let tx_cat = db_cat.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
2025-08-05 00:20:57 +02:00
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Open the obsoleted database
|
|
|
|
|
let db_obs = Database::open(&self.obsoleted_db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
|
|
|
|
let tx_obs = db_obs.begin_read()
|
2025-08-05 00:20:57 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Get table statistics
|
|
|
|
|
let mut catalog_count = 0;
|
|
|
|
|
let mut obsoleted_count = 0;
|
|
|
|
|
|
|
|
|
|
// Count catalog entries
|
2025-08-19 22:43:50 +02:00
|
|
|
if let Ok(table) = tx_cat.open_table(CATALOG_TABLE) {
|
2025-08-05 00:20:57 +02:00
|
|
|
for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? {
|
|
|
|
|
let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?;
|
|
|
|
|
catalog_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Count obsoleted entries (separate DB)
|
|
|
|
|
if let Ok(table) = tx_obs.open_table(OBSOLETED_TABLE) {
|
2025-08-05 00:20:57 +02:00
|
|
|
for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? {
|
|
|
|
|
let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?;
|
|
|
|
|
obsoleted_count += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Print statistics
|
2025-08-19 22:43:50 +02:00
|
|
|
println!("Catalog database path: {}", self.db_path.display());
|
|
|
|
|
println!("Obsoleted database path: {}", self.obsoleted_db_path.display());
|
2025-08-05 00:20:57 +02:00
|
|
|
println!("Catalog directory: {}", self.catalog_dir.display());
|
|
|
|
|
println!("Table statistics:");
|
|
|
|
|
println!(" Catalog table: {} entries", catalog_count);
|
|
|
|
|
println!(" Obsoleted table: {} entries", obsoleted_count);
|
2025-08-19 22:43:50 +02:00
|
|
|
println!("Total entries: {}", catalog_count + obsoleted_count);
|
2025-08-05 00:20:57 +02:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
/// Initialize the catalog database
|
|
|
|
|
pub fn init_db(&self) -> Result<()> {
|
2025-08-19 22:43:50 +02:00
|
|
|
// Ensure parent directories exist
|
|
|
|
|
if let Some(parent) = self.db_path.parent() { fs::create_dir_all(parent)?; }
|
|
|
|
|
if let Some(parent) = self.obsoleted_db_path.parent() { fs::create_dir_all(parent)?; }
|
|
|
|
|
|
|
|
|
|
// Create/open catalog database and tables
|
|
|
|
|
let db_cat = Database::create(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create catalog database: {}", e)))?;
|
|
|
|
|
let tx_cat = db_cat.begin_write()
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
2025-08-19 22:43:50 +02:00
|
|
|
tx_cat.open_table(CATALOG_TABLE)
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create catalog table: {}", e)))?;
|
2025-08-19 22:43:50 +02:00
|
|
|
tx_cat.open_table(INCORPORATE_TABLE)
|
2025-08-19 14:30:55 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create incorporate table: {}", e)))?;
|
2025-08-19 22:43:50 +02:00
|
|
|
tx_cat.commit()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)))?;
|
2025-08-19 14:30:55 +02:00
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Create/open obsoleted database and table
|
|
|
|
|
let db_obs = Database::create(&self.obsoleted_db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create obsoleted database: {}", e)))?;
|
|
|
|
|
let tx_obs = db_obs.begin_write()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
tx_obs.open_table(OBSOLETED_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create obsoleted table: {}", e)))?;
|
|
|
|
|
tx_obs.commit()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e)))?;
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build the catalog from downloaded catalogs
|
|
|
|
|
pub fn build_catalog(&self, publishers: &[String]) -> Result<()> {
|
2025-08-13 19:09:22 +02:00
|
|
|
info!("Building catalog (publishers: {})", publishers.len());
|
|
|
|
|
trace!("Catalog directory: {:?}", self.catalog_dir);
|
|
|
|
|
trace!("Catalog database path: {:?}", self.db_path);
|
2025-08-04 23:01:04 +02:00
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
if publishers.is_empty() {
|
|
|
|
|
return Err(CatalogError::NoPublishers);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Open the databases
|
|
|
|
|
trace!("Opening databases at {:?} and {:?}", self.db_path, self.obsoleted_db_path);
|
|
|
|
|
let db_cat = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
|
|
|
|
let db_obs = Database::open(&self.obsoleted_db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
2025-08-04 22:01:38 +02:00
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Begin writing transactions
|
|
|
|
|
trace!("Beginning write transactions");
|
|
|
|
|
let tx_cat = db_cat.begin_write()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin catalog transaction: {}", e)))?;
|
|
|
|
|
let tx_obs = db_obs.begin_write()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin obsoleted transaction: {}", e)))?;
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Open the catalog table
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Opening catalog table");
|
2025-08-19 22:43:50 +02:00
|
|
|
let mut catalog_table = tx_cat.open_table(CATALOG_TABLE)
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the obsoleted table
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Opening obsoleted table");
|
2025-08-19 22:43:50 +02:00
|
|
|
let mut obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Process each publisher
|
|
|
|
|
for publisher in publishers {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Processing publisher: {}", publisher);
|
2025-08-04 22:01:38 +02:00
|
|
|
let publisher_catalog_dir = self.catalog_dir.join(publisher);
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Publisher catalog directory: {:?}", publisher_catalog_dir);
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Skip if the publisher catalog directory doesn't exist
|
|
|
|
|
if !publisher_catalog_dir.exists() {
|
|
|
|
|
warn!("Publisher catalog directory not found: {}", publisher_catalog_dir.display());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 19:09:22 +02:00
|
|
|
// Determine where catalog parts live. Support both legacy nested layout
|
|
|
|
|
// (publisher/<publisher>/catalog) and flat layout (directly under publisher dir).
|
|
|
|
|
let nested_dir = publisher_catalog_dir.join("publisher").join(publisher).join("catalog");
|
|
|
|
|
let flat_dir = publisher_catalog_dir.clone();
|
|
|
|
|
|
|
|
|
|
let catalog_parts_dir = if nested_dir.exists() { &nested_dir } else { &flat_dir };
|
|
|
|
|
|
|
|
|
|
trace!("Creating catalog manager for publisher: {}", publisher);
|
|
|
|
|
trace!("Catalog parts directory: {:?}", catalog_parts_dir);
|
|
|
|
|
|
|
|
|
|
// Check if the catalog parts directory exists (either layout)
|
2025-08-05 00:44:29 +02:00
|
|
|
if !catalog_parts_dir.exists() {
|
|
|
|
|
warn!("Catalog parts directory not found: {}", catalog_parts_dir.display());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-08-13 19:09:22 +02:00
|
|
|
|
|
|
|
|
let mut catalog_manager = CatalogManager::new(catalog_parts_dir, publisher)
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to create catalog manager: {}", e))))?;
|
|
|
|
|
|
|
|
|
|
// Get all catalog parts
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Getting catalog parts for publisher: {}", publisher);
|
2025-08-04 22:01:38 +02:00
|
|
|
let parts = catalog_manager.attrs().parts.clone();
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Catalog parts: {:?}", parts.keys().collect::<Vec<_>>());
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Load all catalog parts
|
|
|
|
|
for part_name in parts.keys() {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Loading catalog part: {}", part_name);
|
2025-08-04 22:01:38 +02:00
|
|
|
catalog_manager.load_part(part_name)
|
|
|
|
|
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to load catalog part: {}", e))))?;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Process each catalog part in a deterministic order: base, dependency, summary, others
|
|
|
|
|
let mut part_names: Vec<String> = parts.keys().cloned().collect();
|
|
|
|
|
part_names.sort_by_key(|name| {
|
|
|
|
|
if name.contains(".base") { 1 }
|
|
|
|
|
else if name.contains(".dependency") { 0 }
|
|
|
|
|
else if name.contains(".summary") { 2 }
|
|
|
|
|
else { 3 }
|
|
|
|
|
});
|
|
|
|
|
for part_name in part_names {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Processing catalog part: {}", part_name);
|
2025-08-04 22:01:38 +02:00
|
|
|
if let Some(part) = catalog_manager.get_part(&part_name) {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Found catalog part: {}", part_name);
|
|
|
|
|
trace!("Packages in part: {:?}", part.packages.keys().collect::<Vec<_>>());
|
2025-08-19 22:43:50 +02:00
|
|
|
self.process_catalog_part(&mut catalog_table, &mut obsoleted_table, &part_name, part, publisher)?;
|
2025-08-04 23:01:04 +02:00
|
|
|
} else {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Catalog part not found: {}", part_name);
|
2025-08-04 22:01:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drop the tables to release the borrow on tx
|
|
|
|
|
drop(catalog_table);
|
|
|
|
|
drop(obsoleted_table);
|
|
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Commit the transactions
|
|
|
|
|
tx_cat.commit()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)))?;
|
|
|
|
|
tx_obs.commit()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e)))?;
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
info!("Catalog built successfully");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Process a catalog part and add its packages to the catalog
|
|
|
|
|
fn process_catalog_part(
|
|
|
|
|
&self,
|
|
|
|
|
catalog_table: &mut redb::Table<&str, &[u8]>,
|
|
|
|
|
obsoleted_table: &mut redb::Table<&str, &[u8]>,
|
2025-08-19 22:43:50 +02:00
|
|
|
part_name: &str,
|
2025-08-04 22:01:38 +02:00
|
|
|
part: &CatalogPart,
|
|
|
|
|
publisher: &str,
|
|
|
|
|
) -> Result<()> {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Processing catalog part for publisher: {}", publisher);
|
2025-08-04 23:01:04 +02:00
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
// Get packages for this publisher
|
|
|
|
|
if let Some(publisher_packages) = part.packages.get(publisher) {
|
2025-08-13 19:09:22 +02:00
|
|
|
let total_versions: usize = publisher_packages.values().map(|v| v.len()).sum();
|
|
|
|
|
let mut processed: usize = 0;
|
2025-08-19 22:43:50 +02:00
|
|
|
// Count of packages marked obsolete in this part, including those skipped because they were already marked obsolete in earlier parts.
|
|
|
|
|
let mut obsolete_count_incl_skipped: usize = 0;
|
|
|
|
|
let mut skipped_obsolete: usize = 0;
|
2025-08-13 19:09:22 +02:00
|
|
|
let progress_step: usize = 500; // report every N packages
|
|
|
|
|
|
|
|
|
|
trace!(
|
|
|
|
|
"Found {} package stems ({} versions) for publisher {}",
|
|
|
|
|
publisher_packages.len(),
|
|
|
|
|
total_versions,
|
|
|
|
|
publisher
|
|
|
|
|
);
|
2025-08-04 23:01:04 +02:00
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
// Process each package stem
|
|
|
|
|
for (stem, versions) in publisher_packages {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Processing package stem: {} ({} versions)", stem, versions.len());
|
2025-08-04 23:01:04 +02:00
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
// Process each package version
|
|
|
|
|
for version_entry in versions {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("Processing version: {} | actions: {:?}", version_entry.version, version_entry.actions);
|
2025-08-04 23:01:04 +02:00
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
// Create the FMRI
|
|
|
|
|
let version = if !version_entry.version.is_empty() {
|
|
|
|
|
match crate::fmri::Version::parse(&version_entry.version) {
|
2025-08-13 19:09:22 +02:00
|
|
|
Ok(v) => Some(v),
|
2025-08-04 22:01:38 +02:00
|
|
|
Err(e) => {
|
|
|
|
|
warn!("Failed to parse version '{}': {}", version_entry.version, e);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let fmri = Fmri::with_publisher(publisher, stem, version);
|
|
|
|
|
let catalog_key = format!("{}@{}", stem, version_entry.version);
|
|
|
|
|
let obsoleted_key = fmri.to_string();
|
2025-08-19 22:43:50 +02:00
|
|
|
|
|
|
|
|
// If this is not the base part and this package/version was already marked
|
|
|
|
|
// obsolete in an earlier part (present in obsoleted_table) and is NOT present
|
|
|
|
|
// in the catalog_table, skip importing it from this part.
|
|
|
|
|
if !part_name.contains(".base") {
|
|
|
|
|
let has_catalog = matches!(catalog_table.get(catalog_key.as_str()), Ok(Some(_)));
|
|
|
|
|
if !has_catalog {
|
|
|
|
|
let was_obsoleted = matches!(obsoleted_table.get(obsoleted_key.as_str()), Ok(Some(_)));
|
|
|
|
|
if was_obsoleted {
|
|
|
|
|
// Count as obsolete for progress accounting, even though we skip processing
|
|
|
|
|
obsolete_count_incl_skipped += 1;
|
|
|
|
|
skipped_obsolete += 1;
|
|
|
|
|
trace!(
|
|
|
|
|
"Skipping {} from part {} because it is marked obsolete and not present in catalog",
|
|
|
|
|
obsoleted_key,
|
|
|
|
|
part_name
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Check if we already have this package in the catalog
|
2025-08-13 19:09:22 +02:00
|
|
|
let existing_manifest = match catalog_table.get(catalog_key.as_str()) {
|
2025-08-19 22:43:50 +02:00
|
|
|
Ok(Some(bytes)) => Some(decode_manifest_bytes(bytes.value())?),
|
2025-08-13 19:09:22 +02:00
|
|
|
_ => None,
|
2025-08-04 22:01:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create or update the manifest
|
|
|
|
|
let manifest = self.create_or_update_manifest(existing_manifest, version_entry, stem, publisher)?;
|
|
|
|
|
|
|
|
|
|
// Check if the package is obsolete
|
|
|
|
|
let is_obsolete = self.is_package_obsolete(&manifest);
|
2025-08-19 22:43:50 +02:00
|
|
|
if is_obsolete { obsolete_count_incl_skipped += 1; }
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Serialize the manifest
|
|
|
|
|
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
|
|
|
|
|
|
|
|
|
// Store the package in the appropriate table
|
|
|
|
|
if is_obsolete {
|
|
|
|
|
// Store obsolete packages in the obsoleted table with the full FMRI as key
|
|
|
|
|
let empty_bytes: &[u8] = &[0u8; 0];
|
2025-08-13 19:09:22 +02:00
|
|
|
obsoleted_table
|
|
|
|
|
.insert(obsoleted_key.as_str(), empty_bytes)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to insert into obsoleted table: {}", e)))?;
|
2025-08-04 22:01:38 +02:00
|
|
|
} else {
|
|
|
|
|
// Store non-obsolete packages in the catalog table with stem@version as a key
|
2025-08-19 22:43:50 +02:00
|
|
|
let compressed = compress_json_lz4(&manifest_bytes)?;
|
2025-08-13 19:09:22 +02:00
|
|
|
catalog_table
|
2025-08-19 22:43:50 +02:00
|
|
|
.insert(catalog_key.as_str(), compressed.as_slice())
|
2025-08-13 19:09:22 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to insert into catalog table: {}", e)))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processed += 1;
|
|
|
|
|
if processed % progress_step == 0 {
|
|
|
|
|
info!(
|
2025-08-19 22:43:50 +02:00
|
|
|
"Import progress (publisher {}, part {}): {}/{} versions processed ({} obsolete incl. skipped, {} skipped)",
|
2025-08-13 19:09:22 +02:00
|
|
|
publisher,
|
2025-08-19 22:43:50 +02:00
|
|
|
part_name,
|
2025-08-13 19:09:22 +02:00
|
|
|
processed,
|
|
|
|
|
total_versions,
|
2025-08-19 22:43:50 +02:00
|
|
|
obsolete_count_incl_skipped,
|
|
|
|
|
skipped_obsolete
|
2025-08-13 19:09:22 +02:00
|
|
|
);
|
2025-08-04 22:01:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-13 19:09:22 +02:00
|
|
|
|
|
|
|
|
// Final summary for this part/publisher
|
|
|
|
|
info!(
|
2025-08-19 22:43:50 +02:00
|
|
|
"Finished import for publisher {}, part {}: {} versions processed ({} obsolete incl. skipped, {} skipped)",
|
2025-08-13 19:09:22 +02:00
|
|
|
publisher,
|
2025-08-19 22:43:50 +02:00
|
|
|
part_name,
|
2025-08-13 19:09:22 +02:00
|
|
|
processed,
|
2025-08-19 22:43:50 +02:00
|
|
|
obsolete_count_incl_skipped,
|
|
|
|
|
skipped_obsolete
|
2025-08-13 19:09:22 +02:00
|
|
|
);
|
2025-08-04 23:01:04 +02:00
|
|
|
} else {
|
2025-08-13 19:09:22 +02:00
|
|
|
trace!("No packages found for publisher: {}", publisher);
|
2025-08-04 22:01:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create or update a manifest from a package version entry
|
|
|
|
|
fn create_or_update_manifest(
|
|
|
|
|
&self,
|
|
|
|
|
existing_manifest: Option<Manifest>,
|
|
|
|
|
version_entry: &PackageVersionEntry,
|
|
|
|
|
stem: &str,
|
|
|
|
|
publisher: &str,
|
|
|
|
|
) -> Result<Manifest> {
|
|
|
|
|
// Start with the existing manifest or create a new one
|
|
|
|
|
let mut manifest = existing_manifest.unwrap_or_else(Manifest::new);
|
|
|
|
|
|
2025-08-04 23:45:41 +02:00
|
|
|
// Parse and add actions from the version entry
|
|
|
|
|
if let Some(actions) = &version_entry.actions {
|
|
|
|
|
for action_str in actions {
|
2025-08-19 11:06:48 +02:00
|
|
|
// Parse each action string to extract attributes we care about in the catalog
|
2025-08-04 23:45:41 +02:00
|
|
|
if action_str.starts_with("set ") {
|
|
|
|
|
// Format is typically "set name=pkg.key value=value"
|
|
|
|
|
if let Some(name_part) = action_str.split_whitespace().nth(1) {
|
|
|
|
|
if name_part.starts_with("name=") {
|
|
|
|
|
// Extract the key (after "name=")
|
|
|
|
|
let key = &name_part[5..];
|
|
|
|
|
|
|
|
|
|
// Extract the value (after "value=")
|
|
|
|
|
if let Some(value_part) = action_str.split_whitespace().nth(2) {
|
|
|
|
|
if value_part.starts_with("value=") {
|
|
|
|
|
let mut value = &value_part[6..];
|
|
|
|
|
|
|
|
|
|
// Remove quotes if present
|
|
|
|
|
if value.starts_with('"') && value.ends_with('"') {
|
|
|
|
|
value = &value[1..value.len()-1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add or update the attribute in the manifest
|
|
|
|
|
let attr_index = manifest.attributes.iter().position(|attr| attr.key == key);
|
|
|
|
|
if let Some(index) = attr_index {
|
|
|
|
|
manifest.attributes[index].values = vec![value.to_string()];
|
|
|
|
|
} else {
|
|
|
|
|
let mut attr = crate::actions::Attr::default();
|
|
|
|
|
attr.key = key.to_string();
|
|
|
|
|
attr.values = vec![value.to_string()];
|
|
|
|
|
manifest.attributes.push(attr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-19 11:06:48 +02:00
|
|
|
} else if action_str.starts_with("depend ") {
|
|
|
|
|
// Example: "depend fmri=desktop/mate/caja type=require"
|
|
|
|
|
let rest = &action_str[7..]; // strip leading "depend "
|
|
|
|
|
let mut dep_type: String = String::new();
|
|
|
|
|
let mut dep_predicate: Option<crate::fmri::Fmri> = None;
|
|
|
|
|
let mut dep_fmris: Vec<crate::fmri::Fmri> = Vec::new();
|
|
|
|
|
let mut root_image: String = String::new();
|
|
|
|
|
|
|
|
|
|
for tok in rest.split_whitespace() {
|
|
|
|
|
if let Some((k, v)) = tok.split_once('=') {
|
|
|
|
|
match k {
|
|
|
|
|
"type" => dep_type = v.to_string(),
|
|
|
|
|
"predicate" => {
|
|
|
|
|
if let Ok(f) = crate::fmri::Fmri::parse(v) { dep_predicate = Some(f); }
|
|
|
|
|
}
|
|
|
|
|
"fmri" => {
|
|
|
|
|
if let Ok(f) = crate::fmri::Fmri::parse(v) { dep_fmris.push(f); }
|
|
|
|
|
}
|
|
|
|
|
"root-image" => {
|
|
|
|
|
root_image = v.to_string();
|
|
|
|
|
}
|
|
|
|
|
_ => { /* ignore other props for catalog */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For each fmri property, add a Dependency entry
|
|
|
|
|
for f in dep_fmris {
|
|
|
|
|
let mut d = crate::actions::Dependency::default();
|
|
|
|
|
d.fmri = Some(f);
|
|
|
|
|
d.dependency_type = dep_type.clone();
|
|
|
|
|
d.predicate = dep_predicate.clone();
|
|
|
|
|
d.root_image = root_image.clone();
|
|
|
|
|
manifest.dependencies.push(d);
|
|
|
|
|
}
|
2025-08-04 23:45:41 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Ensure the manifest has the correct FMRI attribute
|
|
|
|
|
// Create a Version object from the version string
|
|
|
|
|
let version = if !version_entry.version.is_empty() {
|
|
|
|
|
match crate::fmri::Version::parse(&version_entry.version) {
|
|
|
|
|
Ok(v) => Some(v),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
// Map the FmriError to a CatalogError
|
|
|
|
|
return Err(CatalogError::Repository(
|
|
|
|
|
crate::repository::RepositoryError::Other(
|
|
|
|
|
format!("Invalid version format: {}", e)
|
|
|
|
|
)
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create the FMRI with publisher, stem, and version
|
|
|
|
|
let fmri = Fmri::with_publisher(publisher, stem, version);
|
|
|
|
|
self.ensure_fmri_attribute(&mut manifest, &fmri);
|
|
|
|
|
|
|
|
|
|
Ok(manifest)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ensure the manifest has the correct FMRI attribute
|
|
|
|
|
fn ensure_fmri_attribute(&self, manifest: &mut Manifest, fmri: &Fmri) {
|
|
|
|
|
// Check if the manifest already has an FMRI attribute
|
|
|
|
|
let has_fmri = manifest.attributes.iter().any(|attr| attr.key == "pkg.fmri");
|
|
|
|
|
|
|
|
|
|
// If not, add it
|
|
|
|
|
if !has_fmri {
|
|
|
|
|
let mut attr = crate::actions::Attr::default();
|
|
|
|
|
attr.key = "pkg.fmri".to_string();
|
|
|
|
|
attr.values = vec![fmri.to_string()];
|
|
|
|
|
manifest.attributes.push(attr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check if a package is obsolete
|
|
|
|
|
fn is_package_obsolete(&self, manifest: &Manifest) -> bool {
|
|
|
|
|
manifest.attributes.iter().any(|attr| {
|
|
|
|
|
attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true")
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Query the catalog for packages matching a pattern
|
|
|
|
|
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
|
2025-08-19 22:43:50 +02:00
|
|
|
// Open the catalog database
|
|
|
|
|
let db_cat = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
2025-08-04 22:01:38 +02:00
|
|
|
// Begin a read transaction
|
2025-08-19 22:43:50 +02:00
|
|
|
let tx_cat = db_cat.begin_read()
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the catalog table
|
2025-08-19 22:43:50 +02:00
|
|
|
let catalog_table = tx_cat.open_table(CATALOG_TABLE)
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
|
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// Open the obsoleted database
|
|
|
|
|
let db_obs = Database::open(&self.obsoleted_db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
|
|
|
|
let tx_obs = db_obs.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
let mut results = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Process the catalog table (non-obsolete packages)
|
|
|
|
|
// Iterate through all entries in the table
|
|
|
|
|
for entry_result in catalog_table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? {
|
|
|
|
|
let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?;
|
|
|
|
|
let key_str = key.value();
|
|
|
|
|
|
|
|
|
|
// Skip if the key doesn't match the pattern
|
|
|
|
|
if let Some(pattern) = pattern {
|
|
|
|
|
if !key_str.contains(pattern) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the key to get stem and version
|
|
|
|
|
let parts: Vec<&str> = key_str.split('@').collect();
|
|
|
|
|
if parts.len() != 2 {
|
|
|
|
|
warn!("Invalid key format: {}", key_str);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stem = parts[0];
|
|
|
|
|
let version = parts[1];
|
|
|
|
|
|
|
|
|
|
// Deserialize the manifest
|
2025-08-19 22:43:50 +02:00
|
|
|
let manifest: Manifest = decode_manifest_bytes(value.value())?;
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Extract the publisher from the FMRI attribute
|
|
|
|
|
let publisher = manifest.attributes.iter()
|
|
|
|
|
.find(|attr| attr.key == "pkg.fmri")
|
|
|
|
|
.map(|attr| {
|
|
|
|
|
if let Some(fmri_str) = attr.values.get(0) {
|
|
|
|
|
// Parse the FMRI string
|
|
|
|
|
match Fmri::parse(fmri_str) {
|
|
|
|
|
Ok(fmri) => fmri.publisher.unwrap_or_else(|| "unknown".to_string()),
|
|
|
|
|
Err(_) => "unknown".to_string(),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
"unknown".to_string()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
|
|
|
|
|
|
|
|
// Create a Version object from the version string
|
|
|
|
|
let version_obj = if !version.is_empty() {
|
|
|
|
|
match crate::fmri::Version::parse(version) {
|
|
|
|
|
Ok(v) => Some(v),
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create the FMRI with publisher, stem, and version
|
|
|
|
|
let fmri = Fmri::with_publisher(&publisher, stem, version_obj);
|
|
|
|
|
|
|
|
|
|
// Add to results (non-obsolete)
|
|
|
|
|
results.push(PackageInfo {
|
|
|
|
|
fmri,
|
|
|
|
|
obsolete: false,
|
|
|
|
|
publisher,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process the obsoleted table (obsolete packages)
|
|
|
|
|
// Iterate through all entries in the table
|
|
|
|
|
for entry_result in obsoleted_table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? {
|
|
|
|
|
let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?;
|
|
|
|
|
let key_str = key.value();
|
|
|
|
|
|
|
|
|
|
// Skip if the key doesn't match the pattern
|
|
|
|
|
if let Some(pattern) = pattern {
|
|
|
|
|
if !key_str.contains(pattern) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the key to get the FMRI
|
|
|
|
|
match Fmri::parse(key_str) {
|
|
|
|
|
Ok(fmri) => {
|
|
|
|
|
// Extract the publisher
|
|
|
|
|
let publisher = fmri.publisher.clone().unwrap_or_else(|| "unknown".to_string());
|
|
|
|
|
|
|
|
|
|
// Add to results (obsolete)
|
|
|
|
|
results.push(PackageInfo {
|
|
|
|
|
fmri,
|
|
|
|
|
obsolete: true,
|
|
|
|
|
publisher,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!("Failed to parse FMRI from obsoleted table key: {}: {}", key_str, e);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(results)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get a manifest from the catalog
|
|
|
|
|
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
|
2025-08-19 22:43:50 +02:00
|
|
|
// Open the catalog database
|
|
|
|
|
let db_cat = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?;
|
2025-08-04 22:01:38 +02:00
|
|
|
// Begin a read transaction
|
2025-08-19 22:43:50 +02:00
|
|
|
let tx_cat = db_cat.begin_read()
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the catalog table
|
2025-08-19 22:43:50 +02:00
|
|
|
let catalog_table = tx_cat.open_table(CATALOG_TABLE)
|
2025-08-04 22:01:38 +02:00
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Create the key for the catalog table (stem@version)
|
|
|
|
|
let catalog_key = format!("{}@{}", fmri.stem(), fmri.version());
|
|
|
|
|
|
|
|
|
|
// Try to get the manifest from the catalog table
|
|
|
|
|
if let Ok(Some(bytes)) = catalog_table.get(catalog_key.as_str()) {
|
2025-08-19 22:43:50 +02:00
|
|
|
return Ok(Some(decode_manifest_bytes(bytes.value())?));
|
2025-08-04 22:01:38 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-19 22:43:50 +02:00
|
|
|
// If not found in catalog DB, check obsoleted DB
|
|
|
|
|
let db_obs = Database::open(&self.obsoleted_db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?;
|
|
|
|
|
let tx_obs = db_obs.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
|
|
|
|
let obsoleted_key = fmri.to_string();
|
2025-08-04 22:01:38 +02:00
|
|
|
if let Ok(Some(_)) = obsoleted_table.get(obsoleted_key.as_str()) {
|
|
|
|
|
let mut manifest = Manifest::new();
|
|
|
|
|
let mut attr = crate::actions::Attr::default();
|
|
|
|
|
attr.key = "pkg.fmri".to_string();
|
|
|
|
|
attr.values = vec![fmri.to_string()];
|
|
|
|
|
manifest.attributes.push(attr);
|
|
|
|
|
let mut attr = crate::actions::Attr::default();
|
|
|
|
|
attr.key = "pkg.obsolete".to_string();
|
|
|
|
|
attr.values = vec!["true".to_string()];
|
|
|
|
|
manifest.attributes.push(attr);
|
|
|
|
|
return Ok(Some(manifest));
|
|
|
|
|
}
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|