ips/pkg6repo/src/main.rs

1819 lines
60 KiB
Rust
Raw Normal View History

mod error;
mod pkg5_import;
use error::{Pkg6RepoError, Result};
use pkg5_import::Pkg5Importer;
use clap::{Parser, Subcommand};
use libips::repository::{FileBackend, ReadableRepository, RepositoryVersion, WritableRepository};
2025-07-26 12:54:01 +02:00
use serde::Serialize;
use std::convert::TryFrom;
use std::path::PathBuf;
use tracing::{debug, info};
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::{EnvFilter, fmt};
#[cfg(test)]
mod e2e_tests;
2025-07-26 12:54:01 +02:00
#[cfg(test)]
mod tests;
// Wrapper structs for JSON serialization
#[derive(Serialize)]
struct PropertiesOutput {
#[serde(flatten)]
properties: std::collections::HashMap<String, String>,
}
#[derive(Serialize)]
struct InfoOutput {
publishers: Vec<libips::repository::PublisherInfo>,
}
#[derive(Serialize)]
struct PackagesOutput {
packages: Vec<libips::repository::PackageInfo>,
}
#[derive(Serialize)]
struct SearchOutput {
query: String,
results: Vec<libips::repository::PackageInfo>,
}
#[derive(Serialize)]
struct ObsoletedPackagesOutput {
packages: Vec<String>,
}
#[derive(Serialize)]
struct ObsoletedPackageDetailsOutput {
fmri: String,
status: String,
obsolescence_date: String,
deprecation_message: Option<String>,
obsoleted_by: Option<Vec<String>>,
metadata_version: u32,
content_hash: String,
}
/// pkg6repo - Image Packaging System repository management utility
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
struct App {
#[clap(subcommand)]
command: Commands,
2021-03-21 14:12:03 -03:00
}
#[derive(Subcommand, Debug)]
enum Commands {
/// Create a new package repository
Create {
/// Version of the repository to create
#[clap(long = "repo-version", default_value = "4")]
repo_version: u32,
2025-07-26 12:54:01 +02:00
/// Path or URI of the repository to create
uri_or_path: String,
},
2025-07-26 12:54:01 +02:00
/// Add publishers to a repository
AddPublisher {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Publishers to add
publisher: Vec<String>,
},
2025-07-26 12:54:01 +02:00
/// Remove publishers from a repository
RemovePublisher {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Perform a dry run
#[clap(short = 'n')]
dry_run: bool,
2025-07-26 12:54:01 +02:00
/// Publishers to remove
publisher: Vec<String>,
},
2025-07-26 12:54:01 +02:00
/// Get repository properties
Get {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Output format
#[clap(short = 'F')]
format: Option<String>,
2025-07-26 12:54:01 +02:00
/// Omit headers
#[clap(short = 'H')]
omit_headers: bool,
2025-07-26 12:54:01 +02:00
/// Publisher to get properties for
#[clap(short = 'p')]
publisher: Option<Vec<String>>,
2025-07-26 12:54:01 +02:00
/// SSL key file
#[clap(long)]
key: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// SSL certificate file
#[clap(long)]
cert: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// Properties to get (section/property)
section_property: Option<Vec<String>>,
},
2025-07-26 12:54:01 +02:00
/// Display repository information
Info {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Output format
#[clap(short = 'F')]
format: Option<String>,
2025-07-26 12:54:01 +02:00
/// Omit headers
#[clap(short = 'H')]
omit_headers: bool,
2025-07-26 12:54:01 +02:00
/// Publisher to get information for
#[clap(short = 'p')]
publisher: Option<Vec<String>>,
2025-07-26 12:54:01 +02:00
/// SSL key file
#[clap(long)]
key: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// SSL certificate file
#[clap(long)]
cert: Option<PathBuf>,
},
2025-07-26 12:54:01 +02:00
/// List packages in a repository
List {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Output format
#[clap(short = 'F')]
format: Option<String>,
2025-07-26 12:54:01 +02:00
/// Omit headers
#[clap(short = 'H')]
omit_headers: bool,
2025-07-26 12:54:01 +02:00
/// Publisher to list packages for
#[clap(short = 'p')]
publisher: Option<Vec<String>>,
2025-07-26 12:54:01 +02:00
/// SSL key file
#[clap(long)]
key: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// SSL certificate file
#[clap(long)]
cert: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// Package FMRI patterns to match
pkg_fmri_pattern: Option<Vec<String>>,
},
2025-07-26 12:54:01 +02:00
/// Show contents of packages in a repository
Contents {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Show manifest contents
#[clap(short = 'm')]
manifest: bool,
2025-07-26 12:54:01 +02:00
/// Filter by action type
#[clap(short = 't')]
action_type: Option<Vec<String>>,
2025-07-26 12:54:01 +02:00
/// Publisher to show contents for
#[clap(short = 'p')]
publisher: Option<Vec<String>>,
2025-07-26 12:54:01 +02:00
/// SSL key file
#[clap(long)]
key: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// SSL certificate file
#[clap(long)]
cert: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// Package FMRI patterns to match
pkg_fmri_pattern: Option<Vec<String>>,
},
2025-07-26 12:54:01 +02:00
/// Rebuild repository metadata
Rebuild {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Publisher to rebuild metadata for
#[clap(short = 'p')]
publisher: Option<Vec<String>>,
2025-07-26 12:54:01 +02:00
/// SSL key file
#[clap(long)]
key: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// SSL certificate file
#[clap(long)]
cert: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// Skip catalog rebuild
#[clap(long)]
no_catalog: bool,
2025-07-26 12:54:01 +02:00
/// Skip index rebuild
#[clap(long)]
no_index: bool,
},
2025-07-26 12:54:01 +02:00
/// Refresh repository metadata
Refresh {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Publisher to refresh metadata for
#[clap(short = 'p')]
publisher: Option<Vec<String>>,
2025-07-26 12:54:01 +02:00
/// SSL key file
#[clap(long)]
key: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// SSL certificate file
#[clap(long)]
cert: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// Skip catalog refresh
#[clap(long)]
no_catalog: bool,
2025-07-26 12:54:01 +02:00
/// Skip index refresh
#[clap(long)]
no_index: bool,
},
2025-07-26 12:54:01 +02:00
/// Set repository properties
Set {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Publisher to set properties for
#[clap(short = 'p')]
publisher: Option<String>,
2025-07-26 12:54:01 +02:00
/// Properties to set (section/property=value)
property_value: Vec<String>,
},
2025-07-26 12:54:01 +02:00
/// Search for packages in a repository
Search {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
2025-07-26 12:54:01 +02:00
/// Output format
#[clap(short = 'F')]
format: Option<String>,
2025-07-26 12:54:01 +02:00
/// Omit headers
#[clap(short = 'H')]
omit_headers: bool,
2025-07-26 12:54:01 +02:00
/// Publisher to search packages for
#[clap(short = 'p')]
publisher: Option<Vec<String>>,
2025-07-26 12:54:01 +02:00
/// SSL key file
#[clap(long)]
key: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// SSL certificate file
#[clap(long)]
cert: Option<PathBuf>,
2025-07-26 12:54:01 +02:00
/// Maximum number of results to return
#[clap(short = 'n', long = "limit")]
limit: Option<usize>,
2025-07-26 12:54:01 +02:00
/// Search query
query: String,
},
/// Import a pkg5 repository
ImportPkg5 {
/// Path to the pkg5 repository (directory or p5p archive)
#[clap(short = 's', long)]
source: PathBuf,
/// Path to the destination repository
#[clap(short = 'd', long)]
destination: PathBuf,
/// Publisher to import (defaults to the first publisher found)
#[clap(short = 'p', long)]
publisher: Option<String>,
},
/// Mark a package as obsoleted
ObsoletePackage {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
/// Publisher of the package
#[clap(short = 'p')]
publisher: String,
/// FMRI of the package to mark as obsoleted
#[clap(short = 'f')]
fmri: String,
/// Optional deprecation message explaining why the package is obsoleted
#[clap(short = 'm', long = "message")]
message: Option<String>,
/// Optional list of packages that replace this obsoleted package
#[clap(short = 'r', long = "replaced-by")]
replaced_by: Option<Vec<String>>,
},
/// List obsoleted packages in a repository
ListObsoleted {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
/// Output format
#[clap(short = 'F')]
format: Option<String>,
/// Omit headers
#[clap(short = 'H')]
omit_headers: bool,
/// Publisher to list obsoleted packages for
#[clap(short = 'p')]
publisher: String,
/// Page number (1-based, defaults to 1)
#[clap(long = "page")]
page: Option<usize>,
/// Number of packages per page (defaults to 100, 0 for all)
#[clap(long = "page-size")]
page_size: Option<usize>,
},
/// Show details of an obsoleted package
ShowObsoleted {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
/// Output format
#[clap(short = 'F')]
format: Option<String>,
/// Publisher of the package
#[clap(short = 'p')]
publisher: String,
/// FMRI of the obsoleted package to show
#[clap(short = 'f')]
fmri: String,
},
/// Search for obsoleted packages
SearchObsoleted {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
/// Output format
#[clap(short = 'F')]
format: Option<String>,
/// Omit headers
#[clap(short = 'H')]
omit_headers: bool,
/// Publisher to search obsoleted packages for
#[clap(short = 'p')]
publisher: String,
/// Search pattern (supports glob patterns)
#[clap(short = 'q')]
pattern: String,
/// Maximum number of results to return
#[clap(short = 'n', long = "limit")]
limit: Option<usize>,
},
/// Restore an obsoleted package to the main repository
RestoreObsoleted {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
/// Publisher of the package
#[clap(short = 'p')]
publisher: String,
/// FMRI of the obsoleted package to restore
#[clap(short = 'f')]
fmri: String,
/// Skip rebuilding the catalog after restoration
#[clap(long = "no-rebuild")]
no_rebuild: bool,
},
/// Export obsoleted packages to a file
ExportObsoleted {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
/// Publisher to export obsoleted packages for
#[clap(short = 'p')]
publisher: String,
/// Output file path
#[clap(short = 'o')]
output_file: String,
/// Optional search pattern to filter packages
#[clap(short = 'q')]
pattern: Option<String>,
},
/// Import obsoleted packages from a file
ImportObsoleted {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
/// Input file path
#[clap(short = 'i')]
input_file: String,
/// Override publisher (use this instead of the one in the export file)
#[clap(short = 'p')]
publisher: Option<String>,
},
/// Clean up obsoleted packages older than a specified TTL (time-to-live)
CleanupObsoleted {
/// Path or URI of the repository
#[clap(short = 's')]
repo_uri_or_path: String,
/// Publisher to clean up obsoleted packages for
#[clap(short = 'p')]
publisher: String,
/// TTL in days
#[clap(short = 't', long = "ttl-days", default_value = "90")]
ttl_days: u32,
/// Perform a dry run (don't actually remove packages)
#[clap(short = 'n', long = "dry-run")]
dry_run: bool,
},
}
fn main() -> Result<()> {
// Initialize the tracing subscriber with the default log level as debug and no decorations
// Parse the environment filter first, handling any errors with our custom error type
let env_filter = EnvFilter::builder()
.with_default_directive(LevelFilter::WARN.into())
.from_env()
.map_err(|e| {
Pkg6RepoError::LoggingEnvError(format!("Failed to parse environment filter: {}", e))
})?;
fmt::Subscriber::builder()
.with_max_level(tracing::Level::DEBUG)
.with_env_filter(env_filter)
.without_time()
.with_target(false)
.with_ansi(false)
.with_writer(std::io::stderr)
.init();
let cli = App::parse();
match &cli.command {
2025-07-26 12:54:01 +02:00
Commands::Create {
repo_version,
uri_or_path,
} => {
info!(
2025-07-26 12:54:01 +02:00
"Creating repository version {} at {}",
repo_version, uri_or_path
);
// Convert repo_version to RepositoryVersion
let repo_version_enum = RepositoryVersion::try_from(*repo_version)?;
2025-07-26 12:54:01 +02:00
// Create the repository
let repo = FileBackend::create(uri_or_path, repo_version_enum)?;
2025-07-26 12:54:01 +02:00
info!("Repository created successfully at {}", repo.path.display());
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::AddPublisher {
repo_uri_or_path,
publisher,
} => {
info!(
2025-07-26 12:54:01 +02:00
"Adding publishers {:?} to repository {}",
publisher, repo_uri_or_path
);
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Add each publisher
for p in publisher {
info!("Adding publisher: {}", p);
repo.add_publisher(p)?;
}
2025-07-26 12:54:01 +02:00
info!("Publishers added successfully");
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::RemovePublisher {
repo_uri_or_path,
dry_run,
publisher,
} => {
info!(
2025-07-26 12:54:01 +02:00
"Removing publishers {:?} from repository {}",
publisher, repo_uri_or_path
);
debug!("Dry run: {}", dry_run);
2025-07-26 12:54:01 +02:00
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Remove each publisher
for p in publisher {
info!("Removing publisher: {}", p);
repo.remove_publisher(p, *dry_run)?;
}
2025-07-26 12:54:01 +02:00
if *dry_run {
info!("Dry run completed. No changes were made.");
} else {
info!("Publishers removed successfully");
}
2025-07-26 12:54:01 +02:00
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::Get {
repo_uri_or_path,
format,
omit_headers,
publisher,
section_property,
..
} => {
info!("Getting properties from repository {}", repo_uri_or_path);
2025-07-26 12:54:01 +02:00
// Open the repository
// In a real implementation with RestBackend, the key and cert parameters would be used for SSL authentication
// For now, we're using FileBackend, which doesn't use these parameters
let repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Process the publisher parameter
let pub_names = if let Some(publishers) = publisher {
if !publishers.is_empty() {
publishers.clone()
} else {
Vec::new()
}
} else {
Vec::new()
};
2025-07-26 12:54:01 +02:00
// Filter properties if section_property is specified
let section_filtered_properties = if let Some(section_props) = section_property {
let mut filtered = std::collections::HashMap::new();
2025-07-26 12:54:01 +02:00
for section_prop in section_props {
// Check if the section_property contains a slash (section/property)
if section_prop.contains('/') {
// Exact match for section/property
if let Some(value) = repo.config.properties.get(section_prop) {
filtered.insert(section_prop.clone(), value.clone());
}
} else {
// Match section only
for (key, value) in &repo.config.properties {
if key.starts_with(&format!("{}/", section_prop)) {
filtered.insert(key.clone(), value.clone());
}
}
}
}
2025-07-26 12:54:01 +02:00
filtered
} else {
// No filtering, use all properties
repo.config.properties.clone()
};
2025-07-26 12:54:01 +02:00
// Filter properties by publisher if specified
let filtered_properties = if !pub_names.is_empty() {
let mut filtered = std::collections::HashMap::new();
2025-07-26 12:54:01 +02:00
for (key, value) in &section_filtered_properties {
let parts: Vec<&str> = key.split('/').collect();
if parts.len() == 2 && pub_names.contains(&parts[0].to_string()) {
filtered.insert(key.clone(), value.clone());
}
}
2025-07-26 12:54:01 +02:00
filtered
} else {
section_filtered_properties
};
2025-07-26 12:54:01 +02:00
// Determine the output format
let output_format = format.as_deref().unwrap_or("table");
2025-07-26 12:54:01 +02:00
match output_format {
"table" => {
// Print headers if not omitted
if !omit_headers {
println!("{:<10} {:<10} {:<20}", "SECTION", "PROPERTY", "VALUE");
}
2025-07-26 12:54:01 +02:00
// Print repository properties
for (key, value) in &filtered_properties {
let parts: Vec<&str> = key.split('/').collect();
if parts.len() == 2 {
println!("{:<10} {:<10} {:<20}", parts[0], parts[1], value);
} else {
println!("{:<10} {:<10} {:<20}", "", key, value);
}
}
2025-07-26 12:54:01 +02:00
}
"json" => {
// Create a JSON representation of the properties using serde_json
let properties_output = PropertiesOutput {
properties: repo.config.properties.clone(),
};
2025-07-26 12:54:01 +02:00
// Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&properties_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
2025-07-26 12:54:01 +02:00
println!("{}", json_output);
2025-07-26 12:54:01 +02:00
}
"tsv" => {
// Print headers if not omitted
if !omit_headers {
println!("SECTION\tPROPERTY\tVALUE");
}
2025-07-26 12:54:01 +02:00
// Print repository properties as tab-separated values
for (key, value) in &repo.config.properties {
let parts: Vec<&str> = key.split('/').collect();
if parts.len() == 2 {
println!("{}\t{}\t{}", parts[0], parts[1], value);
} else {
println!("\t{}\t{}", key, value);
}
}
2025-07-26 12:54:01 +02:00
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
));
}
}
2025-07-26 12:54:01 +02:00
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::Info {
repo_uri_or_path,
format,
omit_headers,
publisher,
..
} => {
info!("Displaying info for repository {}", repo_uri_or_path);
2025-07-26 12:54:01 +02:00
// Open the repository
// In a real implementation with RestBackend, the key and cert parameters would be used for SSL authentication
// For now, we're using FileBackend, which doesn't use these parameters
let repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Process the publisher parameter
let pub_names = if let Some(publishers) = publisher {
if !publishers.is_empty() {
publishers.clone()
} else {
Vec::new()
}
} else {
Vec::new()
};
2025-07-26 12:54:01 +02:00
// Get repository info
let mut repo_info = repo.get_info()?;
2025-07-26 12:54:01 +02:00
// Filter publishers if specified
if !pub_names.is_empty() {
repo_info.publishers.retain(|p| pub_names.contains(&p.name));
}
2025-07-26 12:54:01 +02:00
// Determine the output format
let output_format = format.as_deref().unwrap_or("table");
2025-07-26 12:54:01 +02:00
match output_format {
"table" => {
// Print headers if not omitted
if !omit_headers {
2025-07-26 12:54:01 +02:00
println!(
"{:<10} {:<8} {:<6} {:<30}",
"PUBLISHER", "PACKAGES", "STATUS", "UPDATED"
);
}
2025-07-26 12:54:01 +02:00
// Print repository info
for publisher_info in repo_info.publishers {
2025-07-26 12:54:01 +02:00
println!(
"{:<10} {:<8} {:<6} {:<30}",
publisher_info.name,
publisher_info.package_count,
publisher_info.status,
publisher_info.updated
);
}
2025-07-26 12:54:01 +02:00
}
"json" => {
// Create a JSON representation of the repository info using serde_json
let info_output = InfoOutput {
publishers: repo_info.publishers,
};
2025-07-26 12:54:01 +02:00
// Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&info_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
2025-07-26 12:54:01 +02:00
println!("{}", json_output);
2025-07-26 12:54:01 +02:00
}
"tsv" => {
// Print headers if not omitted
if !omit_headers {
println!("PUBLISHER\tPACKAGES\tSTATUS\tUPDATED");
}
2025-07-26 12:54:01 +02:00
// Print repository info as tab-separated values
for publisher_info in repo_info.publishers {
2025-07-26 12:54:01 +02:00
println!(
"{}\t{}\t{}\t{}",
publisher_info.name,
publisher_info.package_count,
publisher_info.status,
publisher_info.updated
);
}
2025-07-26 12:54:01 +02:00
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
));
}
}
2025-07-26 12:54:01 +02:00
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::List {
repo_uri_or_path,
format,
omit_headers,
publisher,
pkg_fmri_pattern,
..
} => {
info!("Listing packages in repository {}", repo_uri_or_path);
2025-07-26 12:54:01 +02:00
// Open the repository
// In a real implementation with RestBackend, the key and cert parameters would be used for SSL authentication
// For now, we're using FileBackend, which doesn't use these parameters
let repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Get the publisher if specified
let pub_option = if let Some(publishers) = publisher {
if !publishers.is_empty() {
Some(publishers[0].as_str())
} else {
None
}
} else {
None
};
2025-07-26 12:54:01 +02:00
// Get the pattern if specified
let pattern_option = if let Some(patterns) = pkg_fmri_pattern {
if !patterns.is_empty() {
Some(patterns[0].as_str())
} else {
None
}
} else {
None
};
2025-07-26 12:54:01 +02:00
// List packages
let packages = repo.list_packages(pub_option, pattern_option)?;
2025-07-26 12:54:01 +02:00
// Determine the output format
let output_format = format.as_deref().unwrap_or("table");
2025-07-26 12:54:01 +02:00
match output_format {
"table" => {
// Print headers if not omitted
if !omit_headers {
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
}
2025-07-26 12:54:01 +02:00
// Print packages
for pkg_info in packages {
// Format version and publisher, handling optional fields
let version_str = pkg_info.fmri.version();
2025-07-26 12:54:01 +02:00
let publisher_str = match &pkg_info.fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
2025-07-26 12:54:01 +02:00
println!(
"{:<30} {:<15} {:<10}",
pkg_info.fmri.stem(),
version_str,
publisher_str
);
}
2025-07-26 12:54:01 +02:00
}
"json" => {
// Create a JSON representation of the packages using serde_json
2025-07-26 12:54:01 +02:00
let packages_output = PackagesOutput { packages };
// Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&packages_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
2025-07-26 12:54:01 +02:00
println!("{}", json_output);
2025-07-26 12:54:01 +02:00
}
"tsv" => {
// Print headers if not omitted
if !omit_headers {
println!("NAME\tVERSION\tPUBLISHER");
}
2025-07-26 12:54:01 +02:00
// Print packages as tab-separated values
for pkg_info in packages {
// Format version and publisher, handling optional fields
let version_str = pkg_info.fmri.version();
2025-07-26 12:54:01 +02:00
let publisher_str = match &pkg_info.fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
2025-07-26 12:54:01 +02:00
println!(
"{}\t{}\t{}",
pkg_info.fmri.stem(),
version_str,
publisher_str
);
}
2025-07-26 12:54:01 +02:00
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
));
}
}
2025-07-26 12:54:01 +02:00
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::Contents {
repo_uri_or_path,
manifest,
action_type,
publisher,
pkg_fmri_pattern,
..
} => {
info!("Showing contents in repository {}", repo_uri_or_path);
2025-07-26 12:54:01 +02:00
// Open the repository
// In a real implementation with RestBackend, the key and cert parameters would be used for SSL authentication
// For now, we're using FileBackend, which doesn't use these parameters
let repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Get the publisher if specified
let pub_option = if let Some(publishers) = publisher {
if !publishers.is_empty() {
Some(publishers[0].as_str())
} else {
None
}
} else {
None
};
2025-07-26 12:54:01 +02:00
// Get the pattern if specified
let pattern_option = if let Some(patterns) = pkg_fmri_pattern {
if !patterns.is_empty() {
Some(patterns[0].as_str())
} else {
None
}
} else {
None
};
2025-07-26 12:54:01 +02:00
// Show contents
2025-07-26 12:54:01 +02:00
let contents =
repo.show_contents(pub_option, pattern_option, action_type.as_deref())?;
// Print contents
for pkg_contents in contents {
// Process files
if let Some(files) = &pkg_contents.files {
for path in files {
if *manifest {
// If a manifest option is specified, print in manifest format
println!("file path={} type={}", path, pkg_contents.package_id);
} else {
// Otherwise, print in table format
2025-07-26 12:54:01 +02:00
println!(
"{:<40} {:<30} {:<10}",
pkg_contents.package_id, path, "file"
);
}
}
}
2025-07-26 12:54:01 +02:00
// Process directories
if let Some(directories) = &pkg_contents.directories {
for path in directories {
if *manifest {
// If a manifest option is specified, print in manifest format
println!("dir path={} type={}", path, pkg_contents.package_id);
} else {
// Otherwise, print in table format
println!("{:<40} {:<30} {:<10}", pkg_contents.package_id, path, "dir");
}
}
}
2025-07-26 12:54:01 +02:00
// Process links
if let Some(links) = &pkg_contents.links {
for path in links {
if *manifest {
// If a manifest option is specified, print in manifest format
println!("link path={} type={}", path, pkg_contents.package_id);
} else {
// Otherwise, print in table format
2025-07-26 12:54:01 +02:00
println!(
"{:<40} {:<30} {:<10}",
pkg_contents.package_id, path, "link"
);
}
}
}
2025-07-26 12:54:01 +02:00
// Process dependencies
if let Some(dependencies) = &pkg_contents.dependencies {
for path in dependencies {
if *manifest {
// If a manifest option is specified, print in manifest format
println!("depend path={} type={}", path, pkg_contents.package_id);
} else {
// Otherwise, print in table format
2025-07-26 12:54:01 +02:00
println!(
"{:<40} {:<30} {:<10}",
pkg_contents.package_id, path, "depend"
);
}
}
}
2025-07-26 12:54:01 +02:00
// Process licenses
if let Some(licenses) = &pkg_contents.licenses {
for path in licenses {
if *manifest {
// If a manifest option is specified, print in manifest format
println!("license path={} type={}", path, pkg_contents.package_id);
} else {
// Otherwise, print in table format
2025-07-26 12:54:01 +02:00
println!(
"{:<40} {:<30} {:<10}",
pkg_contents.package_id, path, "license"
);
}
}
}
}
2025-07-26 12:54:01 +02:00
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::Rebuild {
repo_uri_or_path,
publisher,
no_catalog,
no_index,
..
} => {
info!("Rebuilding repository {}", repo_uri_or_path);
2025-07-26 12:54:01 +02:00
// Open the repository
// In a real implementation with RestBackend, the key and cert parameters would be used for SSL authentication
// For now, we're using FileBackend, which doesn't use these parameters
let repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Get the publisher if specified
let pub_option = if let Some(publishers) = publisher {
if !publishers.is_empty() {
Some(publishers[0].as_str())
} else {
None
}
} else {
None
};
2025-07-26 12:54:01 +02:00
// Rebuild repository metadata
repo.rebuild(pub_option, *no_catalog, *no_index)?;
2025-07-26 12:54:01 +02:00
info!("Repository rebuilt successfully");
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::Refresh {
repo_uri_or_path,
publisher,
no_catalog,
no_index,
..
} => {
info!("Refreshing repository {}", repo_uri_or_path);
2025-07-26 12:54:01 +02:00
// Open the repository
// In a real implementation with RestBackend, the key and cert parameters would be used for SSL authentication
// For now, we're using FileBackend, which doesn't use these parameters
let repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Get the publisher if specified
let pub_option = if let Some(publishers) = publisher {
if !publishers.is_empty() {
Some(publishers[0].as_str())
} else {
None
}
} else {
None
};
2025-07-26 12:54:01 +02:00
// Refresh repository metadata
repo.refresh(pub_option, *no_catalog, *no_index)?;
2025-07-26 12:54:01 +02:00
info!("Repository refreshed successfully");
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::Set {
repo_uri_or_path,
publisher,
property_value,
} => {
info!("Setting properties for repository {}", repo_uri_or_path);
2025-07-26 12:54:01 +02:00
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Process each property=value pair
for prop_val in property_value {
// Split the property=value string
let parts: Vec<&str> = prop_val.split('=').collect();
if parts.len() != 2 {
return Err(Pkg6RepoError::InvalidPropertyValueFormat(
prop_val.to_string(),
));
}
2025-07-26 12:54:01 +02:00
let property = parts[0];
let value = parts[1];
2025-07-26 12:54:01 +02:00
// If a publisher is specified, set the publisher property
if let Some(pub_name) = publisher {
info!(
2025-07-26 12:54:01 +02:00
"Setting publisher property {}/{} = {}",
pub_name, property, value
);
repo.set_publisher_property(pub_name, property, value)?;
} else {
// Otherwise, set the repository property
info!("Setting repository property {} = {}", property, value);
repo.set_property(property, value)?;
}
}
2025-07-26 12:54:01 +02:00
info!("Properties set successfully");
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::Search {
repo_uri_or_path,
format,
omit_headers,
publisher,
limit,
query,
..
} => {
info!("Searching for packages in repository {}", repo_uri_or_path);
2025-07-26 12:54:01 +02:00
// Open the repository
// In a real implementation with RestBackend, the key and cert parameters would be used for SSL authentication
// For now, we're using FileBackend, which doesn't use these parameters
let repo = FileBackend::open(repo_uri_or_path)?;
2025-07-26 12:54:01 +02:00
// Get the publisher if specified
let pub_option = if let Some(publishers) = publisher {
if !publishers.is_empty() {
Some(publishers[0].as_str())
} else {
None
}
} else {
None
};
2025-07-26 12:54:01 +02:00
// Search for packages
let packages = repo.search(&query, pub_option, *limit)?;
2025-07-26 12:54:01 +02:00
// Determine the output format
let output_format = format.as_deref().unwrap_or("table");
2025-07-26 12:54:01 +02:00
match output_format {
"table" => {
// Print headers if not omitted
if !omit_headers {
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
}
2025-07-26 12:54:01 +02:00
// Print packages
for pkg_info in packages {
// Format version and publisher, handling optional fields
let version_str = pkg_info.fmri.version();
2025-07-26 12:54:01 +02:00
let publisher_str = match &pkg_info.fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
2025-07-26 12:54:01 +02:00
println!(
"{:<30} {:<15} {:<10}",
pkg_info.fmri.stem(),
version_str,
publisher_str
);
}
2025-07-26 12:54:01 +02:00
}
"json" => {
// Create a JSON representation of the search results using serde_json
let search_output = SearchOutput {
query: query.clone(),
results: packages,
};
2025-07-26 12:54:01 +02:00
// Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&search_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
2025-07-26 12:54:01 +02:00
println!("{}", json_output);
2025-07-26 12:54:01 +02:00
}
"tsv" => {
// Print headers if not omitted
if !omit_headers {
println!("NAME\tVERSION\tPUBLISHER");
}
2025-07-26 12:54:01 +02:00
// Print packages as tab-separated values
for pkg_info in packages {
// Format version and publisher, handling optional fields
let version_str = pkg_info.fmri.version();
2025-07-26 12:54:01 +02:00
let publisher_str = match &pkg_info.fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
2025-07-26 12:54:01 +02:00
println!(
"{}\t{}\t{}",
pkg_info.fmri.stem(),
version_str,
publisher_str
);
}
2025-07-26 12:54:01 +02:00
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
));
}
}
2025-07-26 12:54:01 +02:00
Ok(())
2025-07-26 12:54:01 +02:00
}
Commands::ImportPkg5 {
source,
destination,
publisher,
} => {
info!(
"Importing pkg5 repository from {} to {}",
source.display(),
destination.display()
);
// Create a new Pkg5Importer
let mut importer = Pkg5Importer::new(source, destination)?;
// Import the repository
importer.import(publisher.as_deref())?;
info!("Repository imported successfully");
Ok(())
}
Commands::ObsoletePackage {
repo_uri_or_path,
publisher,
fmri,
message,
replaced_by,
} => {
info!("Marking package as obsoleted: {}", fmri);
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
// Parse the FMRI
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
// Get the manifest for the package using the helper method
let manifest_path = FileBackend::construct_manifest_path(
&repo.path,
publisher,
parsed_fmri.stem(),
&parsed_fmri.version(),
);
println!("Looking for manifest at: {}", manifest_path.display());
println!("Publisher: {}", publisher);
println!("Stem: {}", parsed_fmri.stem());
println!("Version: {}", parsed_fmri.version());
if !manifest_path.exists() {
return Err(Pkg6RepoError::from(format!(
"Package not found: {}",
parsed_fmri
)));
}
// Read the manifest content
let manifest_content = std::fs::read_to_string(&manifest_path)?;
// Create a new scope for the obsoleted_manager to ensure it's dropped before we call repo.rebuild()
{
// Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?;
// Store the obsoleted package
obsoleted_manager.store_obsoleted_package(
publisher,
&parsed_fmri,
&manifest_content,
replaced_by.clone(),
message.clone(),
)?;
} // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Remove the original package from the repository
std::fs::remove_file(&manifest_path)?;
// Rebuild the catalog to reflect the changes
repo.rebuild(Some(publisher), false, false)?;
info!("Package marked as obsoleted successfully: {}", parsed_fmri);
Ok(())
}
Commands::ListObsoleted {
repo_uri_or_path,
format,
omit_headers,
publisher,
page,
page_size,
} => {
info!("Listing obsoleted packages for publisher: {}", publisher);
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
// Get the obsoleted packages in a new scope to avoid borrowing issues
let paginated_result = {
// Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?;
// List obsoleted packages with pagination
obsoleted_manager.list_obsoleted_packages_paginated(
publisher,
page.clone(),
page_size.clone(),
)?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Determine the output format
let output_format = format.as_deref().unwrap_or("table");
match output_format {
"table" => {
// Print headers if not omitted
if !omit_headers {
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
}
// Print packages
for fmri in &paginated_result.packages {
// Format version and publisher, handling optional fields
let version_str = fmri.version();
let publisher_str = match &fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
println!(
"{:<30} {:<15} {:<10}",
fmri.stem(),
version_str,
publisher_str
);
}
// Print pagination information
println!(
"\nPage {} of {} (Total: {} packages)",
paginated_result.page,
paginated_result.total_pages,
paginated_result.total_count
);
}
"json" => {
// Create a JSON representation of the obsoleted packages with pagination info
#[derive(Serialize)]
struct PaginatedOutput {
packages: Vec<String>,
page: usize,
page_size: usize,
total_pages: usize,
total_count: usize,
}
let packages_str: Vec<String> = paginated_result
.packages
.iter()
.map(|f| f.to_string())
.collect();
let paginated_output = PaginatedOutput {
packages: packages_str,
page: paginated_result.page,
page_size: paginated_result.page_size,
total_pages: paginated_result.total_pages,
total_count: paginated_result.total_count,
};
// Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&paginated_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output);
}
"tsv" => {
// Print headers if not omitted
if !omit_headers {
println!("NAME\tVERSION\tPUBLISHER");
}
// Print packages as tab-separated values
for fmri in &paginated_result.packages {
// Format version and publisher, handling optional fields
let version_str = fmri.version();
let publisher_str = match &fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str);
}
// Print pagination information
println!(
"\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}",
paginated_result.page,
paginated_result.total_pages,
paginated_result.total_count
);
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
));
}
}
Ok(())
}
Commands::ShowObsoleted {
repo_uri_or_path,
format,
publisher,
fmri,
} => {
info!("Showing details of obsoleted package: {}", fmri);
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
// Parse the FMRI
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
// Get the obsoleted package metadata in a new scope to avoid borrowing issues
let metadata = {
// Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?;
// Get the obsoleted package metadata
match obsoleted_manager.get_obsoleted_package_metadata(publisher, &parsed_fmri)? {
Some(metadata) => metadata,
None => {
return Err(Pkg6RepoError::from(format!(
"Obsoleted package not found: {}",
parsed_fmri
)));
}
}
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Determine the output format
let output_format = format.as_deref().unwrap_or("table");
match output_format {
"table" => {
println!("FMRI: {}", metadata.fmri);
println!("Status: {}", metadata.status);
println!("Obsolescence Date: {}", metadata.obsolescence_date);
if let Some(msg) = &metadata.deprecation_message {
println!("Deprecation Message: {}", msg);
}
if let Some(replacements) = &metadata.obsoleted_by {
println!("Replaced By:");
for replacement in replacements {
println!(" {}", replacement);
}
}
println!("Metadata Version: {}", metadata.metadata_version);
println!("Content Hash: {}", metadata.content_hash);
}
"json" => {
// Create a JSON representation of the obsoleted package details
let details_output = ObsoletedPackageDetailsOutput {
fmri: metadata.fmri,
status: metadata.status,
obsolescence_date: metadata.obsolescence_date,
deprecation_message: metadata.deprecation_message,
obsoleted_by: metadata.obsoleted_by,
metadata_version: metadata.metadata_version,
content_hash: metadata.content_hash,
};
// Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&details_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output);
}
"tsv" => {
println!("FMRI\t{}", metadata.fmri);
println!("Status\t{}", metadata.status);
println!("ObsolescenceDate\t{}", metadata.obsolescence_date);
if let Some(msg) = &metadata.deprecation_message {
println!("DeprecationMessage\t{}", msg);
}
if let Some(replacements) = &metadata.obsoleted_by {
for (i, replacement) in replacements.iter().enumerate() {
println!("ReplacedBy{}\t{}", i + 1, replacement);
}
}
println!("MetadataVersion\t{}", metadata.metadata_version);
println!("ContentHash\t{}", metadata.content_hash);
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
));
}
}
Ok(())
}
Commands::SearchObsoleted {
repo_uri_or_path,
format,
omit_headers,
publisher,
pattern,
limit,
} => {
info!(
"Searching for obsoleted packages: {} (publisher: {})",
pattern, publisher
);
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
// Get the obsoleted packages in a new scope to avoid borrowing issues
let obsoleted_packages = {
// Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?;
// Search for obsoleted packages
let mut packages =
obsoleted_manager.search_obsoleted_packages(publisher, pattern)?;
// Apply limit if specified
if let Some(max_results) = limit {
packages.truncate(*max_results);
}
packages
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Determine the output format
let output_format = format.as_deref().unwrap_or("table");
match output_format {
"table" => {
// Print headers if not omitted
if !omit_headers {
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
}
// Print packages
for fmri in obsoleted_packages {
// Format version and publisher, handling optional fields
let version_str = fmri.version();
let publisher_str = match &fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
println!(
"{:<30} {:<15} {:<10}",
fmri.stem(),
version_str,
publisher_str
);
}
}
"json" => {
// Create a JSON representation of the obsoleted packages
let packages_str: Vec<String> =
obsoleted_packages.iter().map(|f| f.to_string()).collect();
let packages_output = ObsoletedPackagesOutput {
packages: packages_str,
};
// Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&packages_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output);
}
"tsv" => {
// Print headers if not omitted
if !omit_headers {
println!("NAME\tVERSION\tPUBLISHER");
}
// Print packages as tab-separated values
for fmri in obsoleted_packages {
// Format version and publisher, handling optional fields
let version_str = fmri.version();
let publisher_str = match &fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str);
}
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
));
}
}
Ok(())
}
Commands::RestoreObsoleted {
repo_uri_or_path,
publisher,
fmri,
no_rebuild,
} => {
info!(
"Restoring obsoleted package: {} (publisher: {})",
fmri, publisher
);
// Parse the FMRI
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
// Get the manifest content and remove the obsoleted package
let manifest_content = {
// Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?;
// Get the manifest content and remove the obsoleted package
obsoleted_manager.get_and_remove_obsoleted_package(publisher, &parsed_fmri)?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Parse the manifest
let manifest = libips::actions::Manifest::parse_string(manifest_content)?;
// Begin a transaction
let mut transaction = repo.begin_transaction()?;
// Set the publisher for the transaction
transaction.set_publisher(publisher);
// Update the manifest in the transaction
transaction.update_manifest(manifest);
// Commit the transaction
transaction.commit()?;
// Rebuild the catalog if not disabled
if !no_rebuild {
info!("Rebuilding catalog...");
repo.rebuild(Some(publisher), false, false)?;
}
info!("Package restored successfully: {}", parsed_fmri);
Ok(())
}
Commands::ExportObsoleted {
repo_uri_or_path,
publisher,
output_file,
pattern,
} => {
info!("Exporting obsoleted packages for publisher: {}", publisher);
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
// Export the obsoleted packages
let count = {
// Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?;
// Export the obsoleted packages
let output_path = PathBuf::from(output_file);
obsoleted_manager.export_obsoleted_packages(
publisher,
pattern.as_deref(),
&output_path,
)?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
info!("Exported {} obsoleted packages to {}", count, output_file);
Ok(())
}
Commands::ImportObsoleted {
repo_uri_or_path,
input_file,
publisher,
} => {
info!("Importing obsoleted packages from {}", input_file);
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
// Import the obsoleted packages
let count = {
// Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?;
// Import the obsoleted packages
let input_path = PathBuf::from(input_file);
obsoleted_manager.import_obsoleted_packages(&input_path, publisher.as_deref())?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
info!("Imported {} obsoleted packages", count);
Ok(())
}
Commands::CleanupObsoleted {
repo_uri_or_path,
publisher,
ttl_days,
dry_run,
} => {
if *dry_run {
info!(
"Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}",
ttl_days, publisher
);
} else {
info!(
"Cleaning up obsoleted packages older than {} days for publisher: {}",
ttl_days, publisher
);
}
// Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?;
// Clean up the obsoleted packages
let count = {
// Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?;
// Clean up the obsoleted packages
obsoleted_manager
.cleanup_obsoleted_packages_older_than_ttl(publisher, *ttl_days, *dry_run)?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
if *dry_run {
info!("Dry run: Would remove {} obsoleted packages", count);
} else {
info!("Successfully removed {} obsoleted packages", count);
}
Ok(())
}
}
}