Add ImageCatalog and InstalledPackages modules for package management

- Introduced `ImageCatalog` for handling catalog-related operations, including initialization, building from publishers, querying, and manifest management.
- Added `InstalledPackages` for managing installed package operations, such as adding, removing, querying, and retrieving manifests.
- Defined database schemas and error types to support both modules.
- Refactored table handling, ensuring consistent transaction scoping with `redb`.
This commit is contained in:
Till Wegmueller 2025-08-04 22:01:38 +02:00
parent e33ccbe6ec
commit e27cd35d8d
No known key found for this signature in database
7 changed files with 1158 additions and 138 deletions

494
libips/src/image/catalog.rs Normal file
View file

@ -0,0 +1,494 @@
use crate::actions::{Manifest};
use crate::fmri::Fmri;
use crate::repository::catalog::{CatalogManager, CatalogPart, PackageVersionEntry};
use miette::Diagnostic;
use redb::{Database, ReadableTable, TableDefinition};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
use tracing::{info, warn};
/// 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");
/// Table definition for the installed packages database
/// Key: full FMRI including publisher (pkg://publisher/stem@version)
/// Value: serialized Manifest
pub const INSTALLED_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("installed");
/// 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>;
/// 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 {
/// Path to the catalog database
db_path: PathBuf,
/// Path to the catalog directory
catalog_dir: PathBuf,
}
impl ImageCatalog {
/// Create a new image catalog
pub fn new<P: AsRef<Path>>(catalog_dir: P, db_path: P) -> Self {
ImageCatalog {
db_path: db_path.as_ref().to_path_buf(),
catalog_dir: catalog_dir.as_ref().to_path_buf(),
}
}
/// Initialize the catalog database
pub fn init_db(&self) -> Result<()> {
// Create a parent directory if it doesn't exist
if let Some(parent) = self.db_path.parent() {
fs::create_dir_all(parent)?;
}
// Open or create the database
let db = Database::create(&self.db_path)
.map_err(|e| CatalogError::Database(format!("Failed to create database: {}", e)))?;
// Create tables
let tx = db.begin_write()
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
tx.open_table(CATALOG_TABLE)
.map_err(|e| CatalogError::Database(format!("Failed to create catalog table: {}", e)))?;
tx.open_table(OBSOLETED_TABLE)
.map_err(|e| CatalogError::Database(format!("Failed to create obsoleted table: {}", e)))?;
tx.commit()
.map_err(|e| CatalogError::Database(format!("Failed to commit transaction: {}", e)))?;
Ok(())
}
/// Build the catalog from downloaded catalogs
pub fn build_catalog(&self, publishers: &[String]) -> Result<()> {
if publishers.is_empty() {
return Err(CatalogError::NoPublishers);
}
// Open the database
let db = Database::open(&self.db_path)
.map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?;
// Begin a writing transaction
let tx = db.begin_write()
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
// Open the catalog table
let mut catalog_table = tx.open_table(CATALOG_TABLE)
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
// Open the obsoleted table
let mut obsoleted_table = tx.open_table(OBSOLETED_TABLE)
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
// Process each publisher
for publisher in publishers {
let publisher_catalog_dir = self.catalog_dir.join(publisher);
// 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;
}
// Create a catalog manager for this publisher
let mut catalog_manager = CatalogManager::new(&publisher_catalog_dir, publisher)
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to create catalog manager: {}", e))))?;
// Get all catalog parts
let parts = catalog_manager.attrs().parts.clone();
// Load all catalog parts
for part_name in parts.keys() {
catalog_manager.load_part(part_name)
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to load catalog part: {}", e))))?;
}
// Process each catalog part
for (part_name, _) in parts {
if let Some(part) = catalog_manager.get_part(&part_name) {
self.process_catalog_part(&mut catalog_table, &mut obsoleted_table, part, publisher)?;
}
}
}
// Drop the tables to release the borrow on tx
drop(catalog_table);
drop(obsoleted_table);
// Commit the transaction
tx.commit()
.map_err(|e| CatalogError::Database(format!("Failed to commit transaction: {}", e)))?;
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]>,
part: &CatalogPart,
publisher: &str,
) -> Result<()> {
// Get packages for this publisher
if let Some(publisher_packages) = part.packages.get(publisher) {
// Process each package stem
for (stem, versions) in publisher_packages {
// Process each package version
for version_entry in versions {
// Create the FMRI
let version = if !version_entry.version.is_empty() {
match crate::fmri::Version::parse(&version_entry.version) {
Ok(v) => Some(v),
Err(e) => {
warn!("Failed to parse version '{}': {}", version_entry.version, e);
continue;
}
}
} else {
None
};
let fmri = Fmri::with_publisher(publisher, stem, version);
// Create the key for the catalog table (stem@version)
let catalog_key = format!("{}@{}", stem, version_entry.version);
// Create the key for the obsoleted table (full FMRI including publisher)
let obsoleted_key = fmri.to_string();
// Check if we already have this package in the catalog
let existing_manifest = if let Ok(bytes) = catalog_table.get(catalog_key.as_str()) {
if let Some(bytes) = bytes {
Some(serde_json::from_slice::<Manifest>(bytes.value())?)
} else {
None
}
} else {
None
};
// 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);
// 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
// We don't store any meaningful values in the obsoleted table as per requirements,
// but we need to provide a valid byte slice
let empty_bytes: &[u8] = &[0u8; 0];
obsoleted_table.insert(obsoleted_key.as_str(), empty_bytes)
.map_err(|e| CatalogError::Database(format!("Failed to insert into obsoleted table: {}", e)))?;
} else {
// Store non-obsolete packages in the catalog table with stem@version as a key
catalog_table.insert(catalog_key.as_str(), manifest_bytes.as_slice())
.map_err(|e| CatalogError::Database(format!("Failed to insert into catalog table: {}", e)))?;
}
}
}
}
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);
// Note: We're skipping the action parsing step as the actions should already be in the manifest
// from the catalog part. The original code tried to parse actions using Action::from_str,
// but this method doesn't exist and add_action is private.
// 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 {
// Check for the pkg.obsolete attribute
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>> {
// Open the database
let db = Database::open(&self.db_path)
.map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction
let tx = db.begin_read()
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
// Open the catalog table
let catalog_table = tx.open_table(CATALOG_TABLE)
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
// Open the obsoleted table
let obsoleted_table = tx.open_table(OBSOLETED_TABLE)
.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
let manifest: Manifest = serde_json::from_slice(value.value())?;
// 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>> {
// Open the database
let db = Database::open(&self.db_path)
.map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction
let tx = db.begin_read()
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
// Open the catalog table
let catalog_table = tx.open_table(CATALOG_TABLE)
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
// Open the obsoleted table
let obsoleted_table = tx.open_table(OBSOLETED_TABLE)
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
// Create the key for the catalog table (stem@version)
let catalog_key = format!("{}@{}", fmri.stem(), fmri.version());
// Create the key for the obsoleted table (full FMRI including publisher)
let obsoleted_key = fmri.to_string();
// Try to get the manifest from the catalog table
if let Ok(Some(bytes)) = catalog_table.get(catalog_key.as_str()) {
return Ok(Some(serde_json::from_slice(bytes.value())?));
}
// Check if the package is in the obsoleted table
if let Ok(Some(_)) = obsoleted_table.get(obsoleted_key.as_str()) {
// The package is obsolete, but we don't store the manifest in the obsoleted table
// We could return a minimal manifest with just the FMRI and obsolete flag
let mut manifest = Manifest::new();
// Add the FMRI attribute
let mut attr = crate::actions::Attr::default();
attr.key = "pkg.fmri".to_string();
attr.values = vec![fmri.to_string()];
manifest.attributes.push(attr);
// Add the obsolete attribute
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));
}
// Manifest not found
Ok(None)
}
}

View file

@ -0,0 +1,291 @@
use crate::actions::Manifest;
use crate::fmri::Fmri;
use miette::Diagnostic;
use redb::{Database, ReadableTable, TableDefinition};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;
use tracing::{info};
/// Table definition for the installed packages database
/// Key: full FMRI including publisher (pkg://publisher/stem@version)
/// Value: serialized Manifest
pub const INSTALLED_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("installed");
/// Errors that can occur when working with the installed packages database
#[derive(Error, Debug, Diagnostic)]
pub enum InstalledError {
#[error("IO error: {0}")]
#[diagnostic(code(ips::installed_error::io))]
IO(#[from] std::io::Error),
#[error("JSON error: {0}")]
#[diagnostic(code(ips::installed_error::json))]
Json(#[from] serde_json::Error),
#[error("Database error: {0}")]
#[diagnostic(code(ips::installed_error::database))]
Database(String),
#[error("FMRI error: {0}")]
#[diagnostic(code(ips::installed_error::fmri))]
Fmri(#[from] crate::fmri::FmriError),
#[error("Package not found: {0}")]
#[diagnostic(code(ips::installed_error::package_not_found))]
PackageNotFound(String),
}
/// Result type for installed packages operations
pub type Result<T> = std::result::Result<T, InstalledError>;
/// Information about an installed package
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledPackageInfo {
/// The FMRI of the package
pub fmri: Fmri,
/// The publisher of the package
pub publisher: String,
}
/// The installed packages database
pub struct InstalledPackages {
/// Path to the installed packages database
db_path: PathBuf,
}
impl InstalledPackages {
// Note on borrowing and redb:
// When using redb, there's a potential borrowing issue when working with transactions and tables.
// The issue occurs because:
// 1. Tables borrow from the transaction they were opened from
// 2. When committing a transaction with tx.commit(), the transaction is moved
// 3. If a table is still borrowing from the transaction when commit() is called, Rust's borrow checker will prevent the move
//
// To fix this issue, we use block scopes {} around table operations to ensure that the table
// objects are dropped (and their borrows released) before committing the transaction.
// This pattern is used in all methods that commit transactions after table operations.
/// Create a new installed packages database
pub fn new<P: AsRef<Path>>(db_path: P) -> Self {
InstalledPackages {
db_path: db_path.as_ref().to_path_buf(),
}
}
/// Initialize the installed packages database
pub fn init_db(&self) -> Result<()> {
// Create a parent directory if it doesn't exist
if let Some(parent) = self.db_path.parent() {
fs::create_dir_all(parent)?;
}
// Open or create the database
let db = Database::create(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to create database: {}", e)))?;
// Create tables
let tx = db.begin_write()
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
tx.open_table(INSTALLED_TABLE)
.map_err(|e| InstalledError::Database(format!("Failed to create installed table: {}", e)))?;
tx.commit()
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
Ok(())
}
/// Add a package to the installed packages database
pub fn add_package(&self, fmri: &Fmri, manifest: &Manifest) -> Result<()> {
// Open the database
let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a writing transaction
let tx = db.begin_write()
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Create the key (full FMRI including publisher)
let key = fmri.to_string();
// Serialize the manifest
let manifest_bytes = serde_json::to_vec(manifest)?;
// Use a block scope to ensure the table is dropped before committing the transaction
{
// Open the installed table
let mut installed_table = tx.open_table(INSTALLED_TABLE)
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
// Insert the package into the installed table
installed_table.insert(key.as_str(), manifest_bytes.as_slice())
.map_err(|e| InstalledError::Database(format!("Failed to insert into installed table: {}", e)))?;
// The table is dropped at the end of this block, releasing its borrow of tx
}
// Commit the transaction
tx.commit()
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
info!("Added package to installed database: {}", key);
Ok(())
}
/// Remove a package from the installed packages database
pub fn remove_package(&self, fmri: &Fmri) -> Result<()> {
// Open the database
let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a writing transaction
let tx = db.begin_write()
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Create the key (full FMRI including publisher)
let key = fmri.to_string();
// Use a block scope to ensure the table is dropped before committing the transaction
{
// Open the installed table
let mut installed_table = tx.open_table(INSTALLED_TABLE)
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
// Check if the package exists
if let Ok(None) = installed_table.get(key.as_str()) {
return Err(InstalledError::PackageNotFound(key));
}
// Remove the package from the installed table
installed_table.remove(key.as_str())
.map_err(|e| InstalledError::Database(format!("Failed to remove from installed table: {}", e)))?;
// The table is dropped at the end of this block, releasing its borrow of tx
}
// Commit the transaction
tx.commit()
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?;
info!("Removed package from installed database: {}", key);
Ok(())
}
/// Query the installed packages database for packages matching a pattern
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<InstalledPackageInfo>> {
// Open the database
let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction
let tx = db.begin_read()
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Use a block scope to ensure the table is dropped when no longer needed
let results = {
// Open the installed table
let installed_table = tx.open_table(INSTALLED_TABLE)
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
let mut results = Vec::new();
// Process the installed table
// Iterate through all entries in the table
for entry_result in installed_table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? {
let (key, _) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed 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
let fmri = Fmri::from_str(key_str)?;
// Get the publisher (handling the Option<String>)
let publisher = fmri.publisher.clone().unwrap_or_else(|| "unknown".to_string());
// Add to results
results.push(InstalledPackageInfo {
fmri,
publisher,
});
}
results
// The table is dropped at the end of this block
};
Ok(results)
}
/// Get a manifest from the installed packages database
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
// Open the database
let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction
let tx = db.begin_read()
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Create the key (full FMRI including publisher)
let key = fmri.to_string();
// Use a block scope to ensure the table is dropped when no longer needed
let manifest_option = {
// Open the installed table
let installed_table = tx.open_table(INSTALLED_TABLE)
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
// Try to get the manifest from the installed table
if let Ok(Some(bytes)) = installed_table.get(key.as_str()) {
Some(serde_json::from_slice(bytes.value())?)
} else {
None
}
// The table is dropped at the end of this block
};
Ok(manifest_option)
}
/// Check if a package is installed
pub fn is_installed(&self, fmri: &Fmri) -> Result<bool> {
// Open the database
let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction
let tx = db.begin_read()
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Create the key (full FMRI including publisher)
let key = fmri.to_string();
// Use a block scope to ensure the table is dropped when no longer needed
let is_installed = {
// Open the installed table
let installed_table = tx.open_table(INSTALLED_TABLE)
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?;
// Check if the package exists
if let Ok(Some(_)) = installed_table.get(key.as_str()) {
true
} else {
false
}
// The table is dropped at the end of this block
};
Ok(is_installed)
}
}

View file

@ -0,0 +1,121 @@
use super::*;
use crate::actions::{Attr, Manifest};
use crate::fmri::Fmri;
use redb::ReadableTable;
use std::str::FromStr;
use tempfile::tempdir;
#[test]
fn test_installed_packages() {
// Create a temporary directory for the test
let temp_dir = tempdir().unwrap();
let image_path = temp_dir.path().join("image");
// Create the image
let image = Image::create_image(&image_path).unwrap();
// Verify that the installed packages database was initialized
assert!(image.installed_db_path().exists());
// Create a test manifest
let mut manifest = Manifest::new();
// Add some attributes to the manifest
let mut attr = Attr::default();
attr.key = "pkg.fmri".to_string();
attr.values = vec!["pkg://test/example/package@1.0".to_string()];
manifest.attributes.push(attr);
let mut attr = Attr::default();
attr.key = "pkg.summary".to_string();
attr.values = vec!["Example package".to_string()];
manifest.attributes.push(attr);
let mut attr = Attr::default();
attr.key = "pkg.description".to_string();
attr.values = vec!["An example package for testing".to_string()];
manifest.attributes.push(attr);
// Create an FMRI for the package
let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap();
// Install the package
image.install_package(&fmri, &manifest).unwrap();
// Verify that the package is installed
assert!(image.is_package_installed(&fmri).unwrap());
// Query the installed packages
let packages = image.query_installed_packages(None).unwrap();
// Verify that the package is in the results
assert_eq!(packages.len(), 1);
assert_eq!(packages[0].fmri.to_string(), "pkg://test/example/package@1.0");
assert_eq!(packages[0].publisher, "test");
// Get the manifest from the installed packages database
let installed_manifest = image.get_manifest_from_installed(&fmri).unwrap().unwrap();
// Verify that the manifest is correct
assert_eq!(installed_manifest.attributes.len(), 3);
// Uninstall the package
image.uninstall_package(&fmri).unwrap();
// Verify that the package is no longer installed
assert!(!image.is_package_installed(&fmri).unwrap());
// Query the installed packages again
let packages = image.query_installed_packages(None).unwrap();
// Verify that there are no packages
assert_eq!(packages.len(), 0);
// Clean up
temp_dir.close().unwrap();
}
#[test]
fn test_installed_packages_key_format() {
// Create a temporary directory for the test
let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("installed.redb");
// Create the installed packages database
let installed = InstalledPackages::new(&db_path);
installed.init_db().unwrap();
// Create a test manifest
let mut manifest = Manifest::new();
// Add some attributes to the manifest
let mut attr = Attr::default();
attr.key = "pkg.fmri".to_string();
attr.values = vec!["pkg://test/example/package@1.0".to_string()];
manifest.attributes.push(attr);
// Create an FMRI for the package
let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap();
// Add the package to the database
installed.add_package(&fmri, &manifest).unwrap();
// Open the database directly to check the key format
let db = Database::open(&db_path).unwrap();
let tx = db.begin_read().unwrap();
let table = tx.open_table(installed::INSTALLED_TABLE).unwrap();
// Iterate through the keys
let mut keys = Vec::new();
for entry in table.iter().unwrap() {
let (key, _) = entry.unwrap();
keys.push(key.value().to_string());
}
// Verify that there is one key and it has the correct format
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], "pkg://test/example/package@1.0");
// Clean up
temp_dir.close().unwrap();
}

View file

@ -4,14 +4,26 @@ mod tests;
use miette::Diagnostic; use miette::Diagnostic;
use properties::*; use properties::*;
use redb::Database;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::{self, File}; use std::fs::{self, File};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
use redb::{Database, TableDefinition};
use crate::repository::{RestBackend, ReadableRepository, RepositoryError}; use crate::repository::{ReadableRepository, RepositoryError, RestBackend};
// Export the catalog module
pub mod catalog;
use catalog::{ImageCatalog, PackageInfo};
// Export the installed packages module
pub mod installed;
use installed::{InstalledPackageInfo, InstalledPackages};
// Include tests
#[cfg(test)]
mod installed_tests;
#[derive(Debug, Error, Diagnostic)] #[derive(Debug, Error, Diagnostic)]
pub enum ImageError { pub enum ImageError {
@ -273,6 +285,11 @@ impl Image {
pub fn catalog_dir(&self) -> PathBuf { pub fn catalog_dir(&self) -> PathBuf {
self.metadata_dir().join("catalog") self.metadata_dir().join("catalog")
} }
/// Returns the path to the catalog database
pub fn catalog_db_path(&self) -> PathBuf {
self.metadata_dir().join("catalog.redb")
}
/// Creates the metadata directory if it doesn't exist /// Creates the metadata directory if it doesn't exist
pub fn create_metadata_dir(&self) -> Result<()> { pub fn create_metadata_dir(&self) -> Result<()> {
@ -311,31 +328,62 @@ impl Image {
pub fn init_installed_db(&self) -> Result<()> { pub fn init_installed_db(&self) -> Result<()> {
let db_path = self.installed_db_path(); let db_path = self.installed_db_path();
// Create the database if it doesn't exist // Create the installed packages database
let db = Database::create(&db_path).map_err(|e| { let installed = InstalledPackages::new(&db_path);
ImageError::Database(format!("Failed to create installed packages database: {}", e)) installed.init_db().map_err(|e| {
})?; ImageError::Database(format!("Failed to initialize installed packages database: {}", e))
})
// Define tables
let packages_table = TableDefinition::<&str, &[u8]>::new("packages");
// Create tables
let tx = db.begin_write().map_err(|e| {
ImageError::Database(format!("Failed to begin transaction: {}", e))
})?;
tx.open_table(packages_table).map_err(|e| {
ImageError::Database(format!("Failed to create packages table: {}", e))
})?;
tx.commit().map_err(|e| {
ImageError::Database(format!("Failed to commit transaction: {}", e))
})?;
Ok(())
} }
/// Download catalogs from all configured publishers /// Add a package to the installed packages database
pub fn install_package(&self, fmri: &crate::fmri::Fmri, manifest: &crate::actions::Manifest) -> Result<()> {
let installed = InstalledPackages::new(self.installed_db_path());
installed.add_package(fmri, manifest).map_err(|e| {
ImageError::Database(format!("Failed to add package to installed database: {}", e))
})
}
/// Remove a package from the installed packages database
pub fn uninstall_package(&self, fmri: &crate::fmri::Fmri) -> Result<()> {
let installed = InstalledPackages::new(self.installed_db_path());
installed.remove_package(fmri).map_err(|e| {
ImageError::Database(format!("Failed to remove package from installed database: {}", e))
})
}
/// Query the installed packages database for packages matching a pattern
pub fn query_installed_packages(&self, pattern: Option<&str>) -> Result<Vec<InstalledPackageInfo>> {
let installed = InstalledPackages::new(self.installed_db_path());
installed.query_packages(pattern).map_err(|e| {
ImageError::Database(format!("Failed to query installed packages: {}", e))
})
}
/// Get a manifest from the installed packages database
pub fn get_manifest_from_installed(&self, fmri: &crate::fmri::Fmri) -> Result<Option<crate::actions::Manifest>> {
let installed = InstalledPackages::new(self.installed_db_path());
installed.get_manifest(fmri).map_err(|e| {
ImageError::Database(format!("Failed to get manifest from installed database: {}", e))
})
}
/// Check if a package is installed
pub fn is_package_installed(&self, fmri: &crate::fmri::Fmri) -> Result<bool> {
let installed = InstalledPackages::new(self.installed_db_path());
installed.is_installed(fmri).map_err(|e| {
ImageError::Database(format!("Failed to check if package is installed: {}", e))
})
}
/// Initialize the catalog database
pub fn init_catalog_db(&self) -> Result<()> {
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path());
catalog.init_db().map_err(|e| {
ImageError::Database(format!("Failed to initialize catalog database: {}", e))
})
}
/// Download catalogs from all configured publishers and build the merged catalog
pub fn download_catalogs(&self) -> Result<()> { pub fn download_catalogs(&self) -> Result<()> {
// Create catalog directory if it doesn't exist // Create catalog directory if it doesn't exist
self.create_catalog_dir()?; self.create_catalog_dir()?;
@ -345,9 +393,45 @@ impl Image {
self.download_publisher_catalog(&publisher.name)?; self.download_publisher_catalog(&publisher.name)?;
} }
// Build the merged catalog
self.build_catalog()?;
Ok(()) Ok(())
} }
/// Build the merged catalog from downloaded catalogs
pub fn build_catalog(&self) -> Result<()> {
// Initialize the catalog database if it doesn't exist
self.init_catalog_db()?;
// Get publisher names
let publisher_names: Vec<String> = self.publishers.iter()
.map(|p| p.name.clone())
.collect();
// Create the catalog and build it
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path());
catalog.build_catalog(&publisher_names).map_err(|e| {
ImageError::Database(format!("Failed to build catalog: {}", e))
})
}
/// Query the catalog for packages matching a pattern
pub fn query_catalog(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path());
catalog.query_packages(pattern).map_err(|e| {
ImageError::Database(format!("Failed to query catalog: {}", e))
})
}
/// Get a manifest from the catalog
pub fn get_manifest_from_catalog(&self, fmri: &crate::fmri::Fmri) -> Result<Option<crate::actions::Manifest>> {
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path());
catalog.get_manifest(fmri).map_err(|e| {
ImageError::Database(format!("Failed to get manifest from catalog: {}", e))
})
}
/// Download catalog for a specific publisher /// Download catalog for a specific publisher
pub fn download_publisher_catalog(&self, publisher_name: &str) -> Result<()> { pub fn download_publisher_catalog(&self, publisher_name: &str) -> Result<()> {
// Get the publisher // Get the publisher
@ -367,10 +451,13 @@ impl Image {
Ok(()) Ok(())
} }
/// Create a new image with the specified publisher /// Create a new image with the basic directory structure
pub fn create_image<P: AsRef<Path>>(path: P, publisher_name: &str, origin: &str) -> Result<Self> { ///
/// This method only creates the image structure without adding publishers or downloading catalogs.
/// Publisher addition and catalog downloading should be handled separately.
pub fn create_image<P: AsRef<Path>>(path: P) -> Result<Self> {
// Create a new image // Create a new image
let mut image = Image::new_full(path.as_ref().to_path_buf()); let image = Image::new_full(path.as_ref().to_path_buf());
// Create the directory structure // Create the directory structure
image.create_metadata_dir()?; image.create_metadata_dir()?;
@ -380,11 +467,11 @@ impl Image {
// Initialize the installed packages database // Initialize the installed packages database
image.init_installed_db()?; image.init_installed_db()?;
// Add the publisher // Initialize the catalog database
image.add_publisher(publisher_name, origin, vec![], true)?; image.init_catalog_db()?;
// Download the catalog // Save the image
image.download_publisher_catalog(publisher_name)?; image.save()?;
Ok(image) Ok(image)
} }

View file

@ -1,114 +1,118 @@
use super::*; use super::*;
use std::path::Path; use std::fs;
use tempfile::tempdir; use tempfile::tempdir;
#[test] #[test]
fn test_image_types() { fn test_image_catalog() {
let full_image = Image::new_full("/"); // Create a temporary directory for the test
let partial_image = Image::new_partial("/tmp/partial"); let temp_dir = tempdir().unwrap();
let image_path = temp_dir.path().join("image");
assert_eq!(*full_image.image_type(), ImageType::Full);
assert_eq!(*partial_image.image_type(), ImageType::Partial);
}
#[test]
fn test_metadata_paths() {
let full_image = Image::new_full("/");
let partial_image = Image::new_partial("/tmp/partial");
assert_eq!(full_image.metadata_dir(), Path::new("/var/pkg"));
assert_eq!(partial_image.metadata_dir(), Path::new("/tmp/partial/.pkg"));
assert_eq!(
full_image.image_json_path(),
Path::new("/var/pkg/pkg6.image.json")
);
assert_eq!(
partial_image.image_json_path(),
Path::new("/tmp/partial/.pkg/pkg6.image.json")
);
}
#[test]
fn test_save_and_load() -> Result<()> {
// Create a temporary directory for testing
let temp_dir = tempdir().expect("Failed to create temp directory");
let temp_path = temp_dir.path();
// Create a full image
let mut full_image = Image::new_full(temp_path);
// Add some test data // Create the image
full_image.props.push(ImageProperty::String("test_prop".to_string())); let image = Image::create_image(&image_path).unwrap();
// Save the image // Verify that the catalog database was initialized
full_image.save()?; assert!(image.catalog_db_path().exists());
// Check that the metadata directory and JSON file were created
let metadata_dir = temp_path.join("var/pkg");
let json_path = metadata_dir.join("pkg6.image.json");
assert!(metadata_dir.exists());
assert!(json_path.exists());
// Load the image
let loaded_image = Image::load(temp_path)?;
// Check that the loaded image matches the original
assert_eq!(*loaded_image.image_type(), ImageType::Full);
assert_eq!(loaded_image.path, full_image.path);
assert_eq!(loaded_image.props.len(), 1);
// Clean up // Clean up
temp_dir.close().expect("Failed to clean up temp directory"); temp_dir.close().unwrap();
Ok(())
} }
#[test] #[test]
fn test_partial_image() -> Result<()> { fn test_catalog_methods() {
// Create a temporary directory for testing // Create a temporary directory for the test
let temp_dir = tempdir().expect("Failed to create temp directory"); let temp_dir = tempdir().unwrap();
let temp_path = temp_dir.path(); let image_path = temp_dir.path().join("image");
// Create a partial image // Create the image
let mut partial_image = Image::new_partial(temp_path); let mut image = Image::create_image(&image_path).unwrap();
// Add some test data // Add a publisher
partial_image.props.push(ImageProperty::Boolean(true)); image.add_publisher("test", "http://example.com/repo", vec![], true).unwrap();
// Save the image // Create the catalog directory structure
partial_image.save()?; let catalog_dir = image.catalog_dir();
let publisher_dir = catalog_dir.join("test");
fs::create_dir_all(&publisher_dir).unwrap();
// Check that the metadata directory and JSON file were created // Create a simple catalog.attrs file
let metadata_dir = temp_path.join(".pkg"); let attrs_content = r#"{
let json_path = metadata_dir.join("pkg6.image.json"); "parts": {
"base": {}
},
"version": 1
}"#;
fs::write(publisher_dir.join("catalog.attrs"), attrs_content).unwrap();
assert!(metadata_dir.exists()); // Create a simple base catalog part
assert!(json_path.exists()); let base_content = r#"{
"packages": {
"test": {
"example/package": [
{
"version": "1.0",
"actions": [
"set name=pkg.fmri value=pkg://test/example/package@1.0",
"set name=pkg.summary value=\"Example package\"",
"set name=pkg.description value=\"An example package for testing\""
]
}
],
"example/obsolete": [
{
"version": "1.0",
"actions": [
"set name=pkg.fmri value=pkg://test/example/obsolete@1.0",
"set name=pkg.summary value=\"Obsolete package\"",
"set name=pkg.obsolete value=true"
]
}
]
}
}
}"#;
fs::write(publisher_dir.join("base"), base_content).unwrap();
// Load the image // Build the catalog
let loaded_image = Image::load(temp_path)?; image.build_catalog().unwrap();
// Check that the loaded image matches the original // Query the catalog
assert_eq!(*loaded_image.image_type(), ImageType::Partial); let packages = image.query_catalog(None).unwrap();
assert_eq!(loaded_image.path, partial_image.path);
assert_eq!(loaded_image.props.len(), 1); // Verify that both non-obsolete and obsolete packages are in the results
assert_eq!(packages.len(), 2);
// Verify that one package is marked as obsolete
let obsolete_packages: Vec<_> = packages.iter().filter(|p| p.obsolete).collect();
assert_eq!(obsolete_packages.len(), 1);
assert_eq!(obsolete_packages[0].fmri.stem(), "example/obsolete");
// Verify that the obsolete package has the full FMRI as key
// This is indirectly verified by checking that the publisher is included in the FMRI
assert_eq!(obsolete_packages[0].fmri.publisher, Some("test".to_string()));
// Verify that one package is not marked as obsolete
let non_obsolete_packages: Vec<_> = packages.iter().filter(|p| !p.obsolete).collect();
assert_eq!(non_obsolete_packages.len(), 1);
assert_eq!(non_obsolete_packages[0].fmri.stem(), "example/package");
// Get the manifest for the non-obsolete package
let fmri = &non_obsolete_packages[0].fmri;
let manifest = image.get_manifest_from_catalog(fmri).unwrap();
assert!(manifest.is_some());
// Get the manifest for the obsolete package
let fmri = &obsolete_packages[0].fmri;
let manifest = image.get_manifest_from_catalog(fmri).unwrap();
assert!(manifest.is_some());
// Verify that the obsolete package's manifest has the obsolete attribute
let manifest = manifest.unwrap();
let is_obsolete = manifest.attributes.iter().any(|attr| {
attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true")
});
assert!(is_obsolete);
// Clean up // Clean up
temp_dir.close().expect("Failed to clean up temp directory"); temp_dir.close().unwrap();
Ok(())
}
#[test]
fn test_invalid_path() {
let result = Image::load("/nonexistent/path");
assert!(result.is_err());
if let Err(ImageError::InvalidPath(_)) = result {
// Expected error
} else {
panic!("Expected InvalidPath error, got {:?}", result);
}
} }

View file

@ -198,7 +198,7 @@ impl From<bincode::Error> for RepositoryError {
} }
} }
mod catalog; pub mod catalog;
mod file_backend; mod file_backend;
mod obsoleted; mod obsoleted;
pub mod progress; pub mod progress;

View file

@ -398,18 +398,19 @@ enum Commands {
/// Create an image /// Create an image
/// ///
/// The image-create command creates a new image. /// The image-create command creates a new image.
/// If publisher and origin are provided, the publisher will be added to the image.
ImageCreate { ImageCreate {
/// Full path to the image to create /// Full path to the image to create
#[clap(short = 'F')] #[clap(short = 'F')]
full_path: PathBuf, full_path: PathBuf,
/// Publisher to use /// Publisher to use (optional)
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: String, publisher: Option<String>,
/// Publisher origin URL /// Publisher origin URL (required if publisher is specified)
#[clap(short = 'g')] #[clap(short = 'g', requires = "publisher")]
origin: String, origin: Option<String>,
}, },
} }
@ -654,6 +655,14 @@ fn main() -> Result<()> {
// Remove the publisher // Remove the publisher
image.remove_publisher(&publisher)?; image.remove_publisher(&publisher)?;
// Refresh the catalog to reflect the current state of all available packages
if let Err(e) = image.download_catalogs() {
error!("Failed to refresh catalog after removing publisher: {}", e);
// Continue execution even if catalog refresh fails
} else {
info!("Catalog refreshed successfully");
}
info!("Publisher {} removed successfully", publisher); info!("Publisher {} removed successfully", publisher);
info!("Unset-publisher completed successfully"); info!("Unset-publisher completed successfully");
Ok(()) Ok(())
@ -801,15 +810,29 @@ fn main() -> Result<()> {
}, },
Commands::ImageCreate { full_path, publisher, origin } => { Commands::ImageCreate { full_path, publisher, origin } => {
info!("Creating image at: {}", full_path.display()); info!("Creating image at: {}", full_path.display());
debug!("Publisher: {}", publisher); debug!("Publisher: {:?}", publisher);
debug!("Origin: {}", origin); debug!("Origin: {:?}", origin);
// Create the image
let image = libips::image::Image::create_image(&full_path, &publisher, &origin)?;
// Create the image (only creates the basic structure)
let mut image = libips::image::Image::create_image(&full_path)?;
info!("Image created successfully at: {}", full_path.display()); info!("Image created successfully at: {}", full_path.display());
info!("Publisher {} configured with origin: {}", publisher, origin);
info!("Catalog downloaded from publisher: {}", publisher); // If publisher and origin are provided, add the publisher and download the catalog
if let (Some(publisher_name), Some(origin_url)) = (publisher.as_ref(), origin.as_ref()) {
info!("Adding publisher {} with origin {}", publisher_name, origin_url);
// Add the publisher
image.add_publisher(publisher_name, origin_url, vec![], true)?;
// Download the catalog
image.download_publisher_catalog(publisher_name)?;
info!("Publisher {} configured with origin: {}", publisher_name, origin_url);
info!("Catalog downloaded from publisher: {}", publisher_name);
} else {
info!("No publisher configured. Use 'pkg6 set-publisher' to add a publisher.");
}
Ok(()) Ok(())
}, },
} }