Introduce PackageInfo struct for structured package data, refactor list_packages to use it, and enhance publisher directory handling

Signed-off-by: Till Wegmueller <toasterson@gmail.com>
This commit is contained in:
Till Wegmueller 2025-07-22 11:57:24 +02:00
parent 7332e0f7b5
commit e3662eaf23
No known key found for this signature in database
5 changed files with 145 additions and 49 deletions

View file

@ -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<Vec<(String, String, String)>> {
fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
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()),
];
// 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));
}
// Filter by pattern if specified
let filtered_packages = if let Some(pat) = pattern {
example_packages.into_iter()
.filter(|(name, _, _)| name.contains(pat))
.collect()
// 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 {
example_packages
pkg_with_pub
};
packages.extend(filtered_packages);
// 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

View file

@ -36,6 +36,17 @@ pub struct RepositoryInfo {
pub publishers: Vec<PublisherInfo>,
}
/// 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<Vec<(String, String, String)>>;
fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result<Vec<PackageInfo>>;
/// Show contents of packages
fn show_contents(&self, publisher: Option<&str>, pattern: Option<&str>, action_types: Option<&[String]>) -> Result<Vec<(String, String, String)>>;

View file

@ -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<Vec<(String, String, String)>> {
fn list_packages(&self, publisher: Option<&str>, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
// 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

View file

@ -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(())

View file

@ -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);
}