From e3662eaf23ee162bc1d7f90f6ed37c756072d7ec Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Tue, 22 Jul 2025 11:57:24 +0200 Subject: [PATCH] Introduce `PackageInfo` struct for structured package data, refactor `list_packages` to use it, and enhance publisher directory handling Signed-off-by: Till Wegmueller --- libips/src/repository/file_backend.rs | 129 +++++++++++++++++++++----- libips/src/repository/mod.rs | 13 ++- libips/src/repository/rest_backend.rs | 34 +++---- pkg6repo/src/main.rs | 6 +- pkg6repo/src/tests.rs | 12 ++- 5 files changed, 145 insertions(+), 49 deletions(-) diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs index bf68e48..48d11ca 100644 --- a/libips/src/repository/file_backend.rs +++ b/libips/src/repository/file_backend.rs @@ -14,12 +14,13 @@ use std::fs::File; use flate2::write::GzEncoder; use flate2::Compression as GzipCompression; use lz4::EncoderBuilder; +use regex::Regex; use crate::actions::{Manifest, File as FileAction}; use crate::digest::Digest; use crate::payload::{Payload, PayloadCompressionAlgorithm}; -use super::{Repository, RepositoryConfig, RepositoryVersion, REPOSITORY_CONFIG_FILENAME, PublisherInfo, RepositoryInfo}; +use super::{Repository, RepositoryConfig, RepositoryVersion, REPOSITORY_CONFIG_FILENAME, PublisherInfo, RepositoryInfo, PackageInfo}; /// Repository implementation that uses the local filesystem pub struct FileBackend { @@ -369,6 +370,22 @@ impl Repository for FileBackend { if !dry_run { self.config.publishers.remove(pos); + // Remove publisher-specific directories and their contents recursively + let catalog_dir = self.path.join("catalog").join(publisher); + let pkg_dir = self.path.join("pkg").join(publisher); + + // Remove the catalog directory if it exists + if catalog_dir.exists() { + fs::remove_dir_all(&catalog_dir) + .map_err(|e| anyhow!("Failed to remove catalog directory: {}", e))?; + } + + // Remove the package directory if it exists + if pkg_dir.exists() { + fs::remove_dir_all(&pkg_dir) + .map_err(|e| anyhow!("Failed to remove package directory: {}", e))?; + } + // Save the updated configuration self.save_config()?; } @@ -463,7 +480,7 @@ impl Repository for FileBackend { } /// List packages in the repository - fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result> { + fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result> { let mut packages = Vec::new(); // Filter publishers if specified @@ -478,25 +495,91 @@ impl Repository for FileBackend { // For each publisher, list packages for pub_name in publishers { - // In a real implementation, we would scan the repository for packages - // For now, we'll just return a placeholder + // Get the publisher's package directory + let publisher_pkg_dir = self.path.join("pkg").join(&pub_name); - // Example package data (name, version, publisher) - let example_packages = vec![ - ("example/package1".to_string(), "1.0.0".to_string(), pub_name.clone()), - ("example/package2".to_string(), "2.0.0".to_string(), pub_name.clone()), - ]; - - // Filter by pattern if specified - let filtered_packages = if let Some(pat) = pattern { - example_packages.into_iter() - .filter(|(name, _, _)| name.contains(pat)) - .collect() - } else { - example_packages - }; - - packages.extend(filtered_packages); + // Check if the publisher directory exists + if publisher_pkg_dir.exists() { + // Verify that the publisher is in the config + if !self.config.publishers.contains(&pub_name) { + return Err(anyhow!("Publisher directory exists but is not in the repository configuration: {}", pub_name)); + } + + // Walk through the directory and collect package manifests + if let Ok(entries) = fs::read_dir(&publisher_pkg_dir) { + for entry in entries.flatten() { + let path = entry.path(); + + // Skip directories, only process files (package manifests) + if path.is_file() { + // Parse the manifest file to get real package information + match Manifest::parse_file(&path) { + Ok(manifest) => { + // Look for the pkg.fmri attribute + for attr in &manifest.attributes { + if attr.key == "pkg.fmri" && !attr.values.is_empty() { + let fmri = &attr.values[0]; + + // Parse the FMRI to extract package name and version + // Format: pkg://publisher/package_name@version + if let Some(pkg_part) = fmri.strip_prefix("pkg://") { + if let Some(at_pos) = pkg_part.find('@') { + let pkg_with_pub = &pkg_part[0..at_pos]; + let version = &pkg_part[at_pos+1..]; + + // Extract package name (may include publisher) + let pkg_name = if let Some(slash_pos) = pkg_with_pub.find('/') { + // Skip publisher part if present + let pub_end = slash_pos + 1; + &pkg_with_pub[pub_end..] + } else { + pkg_with_pub + }; + + // Filter by pattern if specified + if let Some(pat) = pattern { + // Try to compile the pattern as a regex + match Regex::new(pat) { + Ok(regex) => { + // Use regex matching + if !regex.is_match(pkg_name) { + continue; + } + }, + Err(err) => { + // Log the error but fall back to simple string contains + eprintln!("Error compiling regex pattern '{}': {}", pat, err); + if !pkg_name.contains(pat) { + continue; + } + } + } + } + + // Create a PackageInfo struct and add it to the list + packages.push(PackageInfo { + name: pkg_name.to_string(), + version: version.to_string(), + publisher: pub_name.clone(), + }); + + // Found the package info, no need to check other attributes + break; + } + } + } + } + }, + Err(err) => { + // Log the error but continue processing other files + eprintln!("Error parsing manifest file {}: {}", path.display(), err); + } + } + } + } + } + } + // No else clause - we don't return placeholder data anymore } Ok(packages) @@ -513,11 +596,11 @@ impl Repository for FileBackend { // For each package, list contents let mut contents = Vec::new(); - for (pkg_name, pkg_version, _pub_name) in packages { + for pkg_info in packages { // Example content data (package, path, type) let example_contents = vec![ - (format!("{}@{}", pkg_name, pkg_version), "/usr/bin/example".to_string(), "file".to_string()), - (format!("{}@{}", pkg_name, pkg_version), "/usr/share/doc/example".to_string(), "dir".to_string()), + (format!("{}@{}", pkg_info.name, pkg_info.version), "/usr/bin/example".to_string(), "file".to_string()), + (format!("{}@{}", pkg_info.name, pkg_info.version), "/usr/share/doc/example".to_string(), "dir".to_string()), ]; // Filter by action type if specified diff --git a/libips/src/repository/mod.rs b/libips/src/repository/mod.rs index 425c13d..e6752e9 100644 --- a/libips/src/repository/mod.rs +++ b/libips/src/repository/mod.rs @@ -36,6 +36,17 @@ pub struct RepositoryInfo { pub publishers: Vec, } +/// Information about a package in a repository +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackageInfo { + /// Name of the package + pub name: String, + /// Version of the package + pub version: String, + /// Publisher of the package + pub publisher: String, +} + /// Repository version #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum RepositoryVersion { @@ -106,7 +117,7 @@ pub trait Repository { fn set_publisher_property(&mut self, publisher: &str, property: &str, value: &str) -> Result<()>; /// List packages in the repository - fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result>; + fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result>; /// Show contents of packages fn show_contents(&self, publisher: Option<&str>, pattern: Option<&str>, action_types: Option<&[String]>) -> Result>; diff --git a/libips/src/repository/rest_backend.rs b/libips/src/repository/rest_backend.rs index 5c7518c..4a37fcb 100644 --- a/libips/src/repository/rest_backend.rs +++ b/libips/src/repository/rest_backend.rs @@ -6,7 +6,7 @@ use anyhow::{anyhow, Result}; use std::path::{Path, PathBuf}; -use super::{Repository, RepositoryConfig, RepositoryVersion, PublisherInfo, RepositoryInfo}; +use super::{Repository, RepositoryConfig, RepositoryVersion, PublisherInfo, RepositoryInfo, PackageInfo}; /// Repository implementation that uses a REST API pub struct RestBackend { @@ -164,7 +164,7 @@ impl Repository for RestBackend { } /// List packages in the repository - fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result> { + fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result> { // This is a stub implementation // In a real implementation, we would make a REST API call to list packages @@ -182,24 +182,16 @@ impl Repository for RestBackend { // For each publisher, list packages for pub_name in publishers { - // In a real implementation, we would get this information from the REST API + // In a real implementation, we would make a REST API call to get package information + // The API call would return a list of packages with their names, versions, and other metadata + // We would then parse this information and create PackageInfo structs - // Example package data (name, version, publisher) - let example_packages = vec![ - ("example/package1".to_string(), "1.0.0".to_string(), pub_name.clone()), - ("example/package2".to_string(), "2.0.0".to_string(), pub_name.clone()), - ]; + // For now, we return an empty list since we don't want to return placeholder data + // and we don't have a real API to call - // Filter by pattern if specified - let filtered_packages = if let Some(pat) = pattern { - example_packages.into_iter() - .filter(|(name, _, _)| name.contains(pat)) - .collect() - } else { - example_packages - }; - - packages.extend(filtered_packages); + // If pattern filtering is needed, it would be applied here to the results from the API + // When implementing, use the regex crate to handle user-provided regexp patterns properly, + // similar to the implementation in file_backend.rs } Ok(packages) @@ -216,13 +208,13 @@ impl Repository for RestBackend { // For each package, list contents let mut contents = Vec::new(); - for (pkg_name, pkg_version, _) in packages { + for pkg_info in packages { // In a real implementation, we would get this information from the REST API // Example content data (package, path, type) let example_contents = vec![ - (format!("{}@{}", pkg_name, pkg_version), "/usr/bin/example".to_string(), "file".to_string()), - (format!("{}@{}", pkg_name, pkg_version), "/usr/share/doc/example".to_string(), "dir".to_string()), + (format!("{}@{}", pkg_info.name, pkg_info.version), "/usr/bin/example".to_string(), "file".to_string()), + (format!("{}@{}", pkg_info.name, pkg_info.version), "/usr/share/doc/example".to_string(), "dir".to_string()), ]; // Filter by action type if specified diff --git a/pkg6repo/src/main.rs b/pkg6repo/src/main.rs index 0210a0b..118fd59 100644 --- a/pkg6repo/src/main.rs +++ b/pkg6repo/src/main.rs @@ -3,7 +3,7 @@ use anyhow::{Result, anyhow}; use std::path::PathBuf; use std::convert::TryFrom; -use libips::repository::{Repository, RepositoryVersion, FileBackend, PublisherInfo, RepositoryInfo}; +use libips::repository::{Repository, RepositoryVersion, FileBackend, PublisherInfo, RepositoryInfo, PackageInfo}; #[cfg(test)] mod tests; @@ -377,8 +377,8 @@ fn main() -> Result<()> { } // Print packages - for (name, version, publisher) in packages { - println!("{:<30} {:<15} {:<10}", name, version, publisher); + for pkg_info in packages { + println!("{:<30} {:<15} {:<10}", pkg_info.name, pkg_info.version, pkg_info.publisher); } Ok(()) diff --git a/pkg6repo/src/tests.rs b/pkg6repo/src/tests.rs index abc74c2..65b6dc5 100644 --- a/pkg6repo/src/tests.rs +++ b/pkg6repo/src/tests.rs @@ -91,12 +91,22 @@ mod tests { // Check that the publisher was added assert!(repo.config.publishers.contains(&"example.com".to_string())); + // Check that the publisher directories were created + let catalog_dir = repo_path.join("catalog").join("example.com"); + let pkg_dir = repo_path.join("pkg").join("example.com"); + assert!(catalog_dir.exists(), "Catalog directory should exist after adding publisher"); + assert!(pkg_dir.exists(), "Package directory should exist after adding publisher"); + // Remove the publisher repo.remove_publisher("example.com", false).unwrap(); - // Check that the publisher was removed + // Check that the publisher was removed from the configuration assert!(!repo.config.publishers.contains(&"example.com".to_string())); + // Check that the publisher directories were removed + assert!(!catalog_dir.exists(), "Catalog directory should not exist after removing publisher"); + assert!(!pkg_dir.exists(), "Package directory should not exist after removing publisher"); + // Clean up cleanup_test_dir(&test_dir); }