mod error; use error::{Pkg6RepoError, Result}; use clap::{Parser, Subcommand}; use serde::Serialize; use std::convert::TryFrom; use std::path::PathBuf; use libips::repository::{FileBackend, ReadableRepository, RepositoryVersion, WritableRepository}; #[cfg(test)] mod e2e_tests; #[cfg(test)] mod tests; // Wrapper structs for JSON serialization #[derive(Serialize)] struct PropertiesOutput { #[serde(flatten)] properties: std::collections::HashMap, } #[derive(Serialize)] struct InfoOutput { publishers: Vec, } #[derive(Serialize)] struct PackagesOutput { packages: Vec, } #[derive(Serialize)] struct SearchOutput { query: String, results: Vec, } /// 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, } #[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, /// Path or URI of the repository to create uri_or_path: String, }, /// Add publishers to a repository AddPublisher { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Publishers to add publisher: Vec, }, /// Remove publishers from a repository RemovePublisher { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Perform a dry run #[clap(short = 'n')] dry_run: bool, /// Wait for the operation to complete #[clap(long)] synchronous: bool, /// Publishers to remove publisher: Vec, }, /// Get repository properties Get { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Output format #[clap(short = 'F')] format: Option, /// Omit headers #[clap(short = 'H')] omit_headers: bool, /// Publisher to get properties for #[clap(short = 'p')] publisher: Option>, /// SSL key file #[clap(long)] key: Option, /// SSL certificate file #[clap(long)] cert: Option, /// Properties to get (section/property) section_property: Option>, }, /// Display repository information Info { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Output format #[clap(short = 'F')] format: Option, /// Omit headers #[clap(short = 'H')] omit_headers: bool, /// Publisher to get information for #[clap(short = 'p')] publisher: Option>, /// SSL key file #[clap(long)] key: Option, /// SSL certificate file #[clap(long)] cert: Option, }, /// List packages in a repository List { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Output format #[clap(short = 'F')] format: Option, /// Omit headers #[clap(short = 'H')] omit_headers: bool, /// Publisher to list packages for #[clap(short = 'p')] publisher: Option>, /// SSL key file #[clap(long)] key: Option, /// SSL certificate file #[clap(long)] cert: Option, /// Package FMRI patterns to match pkg_fmri_pattern: Option>, }, /// Show contents of packages in a repository Contents { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Show manifest contents #[clap(short = 'm')] manifest: bool, /// Filter by action type #[clap(short = 't')] action_type: Option>, /// Publisher to show contents for #[clap(short = 'p')] publisher: Option>, /// SSL key file #[clap(long)] key: Option, /// SSL certificate file #[clap(long)] cert: Option, /// Package FMRI patterns to match pkg_fmri_pattern: Option>, }, /// Rebuild repository metadata Rebuild { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Publisher to rebuild metadata for #[clap(short = 'p')] publisher: Option>, /// SSL key file #[clap(long)] key: Option, /// SSL certificate file #[clap(long)] cert: Option, /// Skip catalog rebuild #[clap(long)] no_catalog: bool, /// Skip index rebuild #[clap(long)] no_index: bool, }, /// Refresh repository metadata Refresh { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Publisher to refresh metadata for #[clap(short = 'p')] publisher: Option>, /// SSL key file #[clap(long)] key: Option, /// SSL certificate file #[clap(long)] cert: Option, /// Skip catalog refresh #[clap(long)] no_catalog: bool, /// Skip index refresh #[clap(long)] no_index: bool, }, /// Set repository properties Set { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Publisher to set properties for #[clap(short = 'p')] publisher: Option, /// Properties to set (section/property=value) property_value: Vec, }, /// Search for packages in a repository Search { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, /// Output format #[clap(short = 'F')] format: Option, /// Omit headers #[clap(short = 'H')] omit_headers: bool, /// Publisher to search packages for #[clap(short = 'p')] publisher: Option>, /// SSL key file #[clap(long)] key: Option, /// SSL certificate file #[clap(long)] cert: Option, /// Maximum number of results to return #[clap(short = 'n', long = "limit")] limit: Option, /// Search query query: String, }, } fn main() -> Result<()> { let cli = App::parse(); match &cli.command { Commands::Create { repo_version, uri_or_path, } => { println!( "Creating repository version {} at {}", repo_version, uri_or_path ); // Convert repo_version to RepositoryVersion let repo_version_enum = RepositoryVersion::try_from(*repo_version)?; // Create the repository let repo = FileBackend::create(uri_or_path, repo_version_enum)?; println!("Repository created successfully at {}", repo.path.display()); Ok(()) } Commands::AddPublisher { repo_uri_or_path, publisher, } => { println!( "Adding publishers {:?} to repository {}", publisher, repo_uri_or_path ); // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; // Add each publisher for p in publisher { println!("Adding publisher: {}", p); repo.add_publisher(p)?; } println!("Publishers added successfully"); Ok(()) } Commands::RemovePublisher { repo_uri_or_path, dry_run, synchronous, publisher, } => { println!( "Removing publishers {:?} from repository {}", publisher, repo_uri_or_path ); println!("Dry run: {}, Synchronous: {}", dry_run, synchronous); // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; // Remove each publisher for p in publisher { println!("Removing publisher: {}", p); repo.remove_publisher(p, *dry_run)?; } // The synchronous parameter is used to wait for the operation to complete before returning // For FileBackend, operations are already synchronous, so this parameter doesn't have any effect // For RestBackend, this would wait for the server to complete the operation before returning if *synchronous { println!("Operation completed synchronously"); } if *dry_run { println!("Dry run completed. No changes were made."); } else { println!("Publishers removed successfully"); } Ok(()) } Commands::Get { repo_uri_or_path, format, omit_headers, publisher, section_property, .. } => { println!("Getting properties from repository {}", repo_uri_or_path); // 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)?; // Process the publisher parameter let pub_names = if let Some(publishers) = publisher { if !publishers.is_empty() { publishers.clone() } else { Vec::new() } } else { Vec::new() }; // 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(); 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()); } } } } filtered } else { // No filtering, use all properties repo.config.properties.clone() }; // Filter properties by publisher if specified let filtered_properties = if !pub_names.is_empty() { let mut filtered = std::collections::HashMap::new(); for (key, value) in §ion_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()); } } filtered } else { section_filtered_properties }; // 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!("{:<10} {:<10} {:<20}", "SECTION", "PROPERTY", "VALUE"); } // 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); } } } "json" => { // Create a JSON representation of the properties using serde_json let properties_output = PropertiesOutput { properties: repo.config.properties.clone(), }; // Serialize to pretty-printed JSON let json_output = serde_json::to_string_pretty(&properties_output) .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); println!("{}", json_output); } "tsv" => { // Print headers if not omitted if !omit_headers { println!("SECTION\tPROPERTY\tVALUE"); } // 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); } } } _ => { return Err(Pkg6RepoError::UnsupportedOutputFormat(output_format.to_string())); } } Ok(()) } Commands::Info { repo_uri_or_path, format, omit_headers, publisher, .. } => { println!("Displaying info for repository {}", repo_uri_or_path); // 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)?; // Process the publisher parameter let pub_names = if let Some(publishers) = publisher { if !publishers.is_empty() { publishers.clone() } else { Vec::new() } } else { Vec::new() }; // Get repository info let mut repo_info = repo.get_info()?; // Filter publishers if specified if !pub_names.is_empty() { repo_info.publishers.retain(|p| pub_names.contains(&p.name)); } // 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!( "{:<10} {:<8} {:<6} {:<30}", "PUBLISHER", "PACKAGES", "STATUS", "UPDATED" ); } // Print repository info for publisher_info in repo_info.publishers { println!( "{:<10} {:<8} {:<6} {:<30}", publisher_info.name, publisher_info.package_count, publisher_info.status, publisher_info.updated ); } } "json" => { // Create a JSON representation of the repository info using serde_json let info_output = InfoOutput { publishers: repo_info.publishers, }; // Serialize to pretty-printed JSON let json_output = serde_json::to_string_pretty(&info_output) .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); println!("{}", json_output); } "tsv" => { // Print headers if not omitted if !omit_headers { println!("PUBLISHER\tPACKAGES\tSTATUS\tUPDATED"); } // Print repository info as tab-separated values for publisher_info in repo_info.publishers { println!( "{}\t{}\t{}\t{}", publisher_info.name, publisher_info.package_count, publisher_info.status, publisher_info.updated ); } } _ => { return Err(Pkg6RepoError::UnsupportedOutputFormat(output_format.to_string())); } } Ok(()) } Commands::List { repo_uri_or_path, format, omit_headers, publisher, pkg_fmri_pattern, .. } => { println!("Listing packages in repository {}", repo_uri_or_path); // 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)?; // 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 }; // 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 }; // List packages let packages = repo.list_packages(pub_option, pattern_option)?; // 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 pkg_info in packages { // Format version and publisher, handling optional fields let version_str = pkg_info.fmri.version(); let publisher_str = match &pkg_info.fmri.publisher { Some(publisher) => publisher.clone(), None => String::new(), }; println!( "{:<30} {:<15} {:<10}", pkg_info.fmri.stem(), version_str, publisher_str ); } } "json" => { // Create a JSON representation of the packages using serde_json 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)); println!("{}", json_output); } "tsv" => { // Print headers if not omitted if !omit_headers { println!("NAME\tVERSION\tPUBLISHER"); } // 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(); let publisher_str = match &pkg_info.fmri.publisher { Some(publisher) => publisher.clone(), None => String::new(), }; println!( "{}\t{}\t{}", pkg_info.fmri.stem(), version_str, publisher_str ); } } _ => { return Err(Pkg6RepoError::UnsupportedOutputFormat(output_format.to_string())); } } Ok(()) } Commands::Contents { repo_uri_or_path, manifest, action_type, publisher, pkg_fmri_pattern, .. } => { println!("Showing contents in repository {}", repo_uri_or_path); // 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)?; // 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 }; // 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 }; // Show contents 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 println!( "{:<40} {:<30} {:<10}", pkg_contents.package_id, path, "file" ); } } } // 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"); } } } // 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 println!( "{:<40} {:<30} {:<10}", pkg_contents.package_id, path, "link" ); } } } // 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 println!( "{:<40} {:<30} {:<10}", pkg_contents.package_id, path, "depend" ); } } } // 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 println!( "{:<40} {:<30} {:<10}", pkg_contents.package_id, path, "license" ); } } } } Ok(()) } Commands::Rebuild { repo_uri_or_path, publisher, no_catalog, no_index, .. } => { println!("Rebuilding repository {}", repo_uri_or_path); // 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)?; // 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 }; // Rebuild repository metadata repo.rebuild(pub_option, *no_catalog, *no_index)?; println!("Repository rebuilt successfully"); Ok(()) } Commands::Refresh { repo_uri_or_path, publisher, no_catalog, no_index, .. } => { println!("Refreshing repository {}", repo_uri_or_path); // 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)?; // 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 }; // Refresh repository metadata repo.refresh(pub_option, *no_catalog, *no_index)?; println!("Repository refreshed successfully"); Ok(()) } Commands::Set { repo_uri_or_path, publisher, property_value, } => { println!("Setting properties for repository {}", repo_uri_or_path); // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; // 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())); } let property = parts[0]; let value = parts[1]; // If a publisher is specified, set the publisher property if let Some(pub_name) = publisher { println!( "Setting publisher property {}/{} = {}", pub_name, property, value ); repo.set_publisher_property(pub_name, property, value)?; } else { // Otherwise, set the repository property println!("Setting repository property {} = {}", property, value); repo.set_property(property, value)?; } } println!("Properties set successfully"); Ok(()) } Commands::Search { repo_uri_or_path, format, omit_headers, publisher, limit, query, .. } => { println!("Searching for packages in repository {}", repo_uri_or_path); // 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)?; // 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 }; // Search for packages let packages = repo.search(&query, pub_option, *limit)?; // 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 pkg_info in packages { // Format version and publisher, handling optional fields let version_str = pkg_info.fmri.version(); let publisher_str = match &pkg_info.fmri.publisher { Some(publisher) => publisher.clone(), None => String::new(), }; println!( "{:<30} {:<15} {:<10}", pkg_info.fmri.stem(), version_str, publisher_str ); } } "json" => { // Create a JSON representation of the search results using serde_json let search_output = SearchOutput { query: query.clone(), results: packages, }; // Serialize to pretty-printed JSON let json_output = serde_json::to_string_pretty(&search_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 pkg_info in packages { // Format version and publisher, handling optional fields let version_str = pkg_info.fmri.version(); let publisher_str = match &pkg_info.fmri.publisher { Some(publisher) => publisher.clone(), None => String::new(), }; println!( "{}\t{}\t{}", pkg_info.fmri.stem(), version_str, publisher_str ); } } _ => { return Err(Pkg6RepoError::UnsupportedOutputFormat(output_format.to_string())); } } Ok(()) } } }