Introduce obsoleted package management system in IPS

- Add `obsoleted.rs` module to handle storing, metadata management, and operations for obsoleted packages.
- Implement commands for marking, listing, searching, restoring, exporting, and importing obsoleted packages (`pkg6repo`).
- Enhance `RepositoryError` with `From` implementations for various error types to manage database and serialization-related errors.
- Introduce reusable data structures for obsoleted package metadata and export representation.
- Update `Cargo.toml` and `Cargo.lock` to include new dependencies (`redb`, `bincode`, etc.).
- Document obsoleted package workflow and integration details in `doc/obsoleted_packages.md` for contributors.
- Refactor repository internals to integrate obsoleted package support without disrupting existing workflow.
- Add robust error handling, logging, and pagination for enhanced usability and scalability.
This commit is contained in:
Till Wegmueller 2025-07-29 16:16:12 +02:00
parent 88b55c4a70
commit 9b2f74c5c1
No known key found for this signature in database
11 changed files with 3903 additions and 18 deletions

20
Cargo.lock generated
View file

@ -144,6 +144,15 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -954,6 +963,7 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
name = "libips" name = "libips"
version = "0.1.2" version = "0.1.2"
dependencies = [ dependencies = [
"bincode",
"chrono", "chrono",
"diff-struct", "diff-struct",
"flate2", "flate2",
@ -963,6 +973,7 @@ dependencies = [
"object 0.23.0", "object 0.23.0",
"pest", "pest",
"pest_derive", "pest_derive",
"redb",
"regex", "regex",
"semver", "semver",
"serde", "serde",
@ -1504,6 +1515,15 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "redb"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd7f82ecd6ba647a39dd1a7172b8a1cd9453c0adee6da20cb553d83a9a460fa5"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "redox_users" name = "redox_users"
version = "0.4.6" version = "0.4.6"

54
doc/Obsoletion Index.txt Normal file

File diff suppressed because one or more lines are too long

432
doc/obsoleted_packages.md Normal file
View file

@ -0,0 +1,432 @@
# Obsoleted Packages in IPS
This document describes the handling of obsoleted packages in the Image Packaging System (IPS).
## Overview
Obsoleted packages are packages that are no longer maintained or have been replaced by other packages. In previous versions of IPS, obsoleted packages were marked with the `pkg.obsolete` attribute in their manifest, but they remained in the main package repository. This approach had several drawbacks:
1. Obsoleted packages cluttered the repository and catalog
2. They were still visible in package listings and searches
3. There was no structured way to store metadata about why a package was obsoleted or what replaced it
The new approach stores obsoleted packages in a dedicated directory structure, separate from the main package repository. This provides several benefits:
1. Keeps the main repository clean and focused on active packages
2. Provides a structured way to store metadata about obsoleted packages
3. Allows for better organization and management of obsoleted packages
4. Preserves the original manifest for reference
## Directory Structure
Obsoleted packages are stored in the following directory structure:
```
<repository>/obsoleted/
<publisher>/
<package-stem>/
<version>.json # Metadata about the obsoleted package
<version>.manifest # Original manifest of the obsoleted package
```
For example, an obsoleted package `pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3,5.11-2014.0.1.1:20250628T100651Z` would be stored as:
```
<repository>/obsoleted/
openindiana.org/
library/perl-5/postgres-dbi-5100/
2.19.3%2C5.11-2014.0.1.1%3A20250628T100651Z.json
2.19.3%2C5.11-2014.0.1.1%3A20250628T100651Z.manifest
```
## Metadata Format
The metadata for an obsoleted package is stored in a JSON file with the following structure:
```json
{
"fmri": "pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3,5.11-2014.0.1.1:20250628T100651Z",
"status": "obsolete",
"obsolescence_date": "2025-07-29T12:22:00Z",
"deprecation_message": "This package is deprecated. Use library/perl-5/postgres-dbi instead.",
"obsoleted_by": [
"pkg://openindiana.org/library/perl-5/postgres-dbi@3.0.0"
],
"metadata_version": 1,
"content_hash": "sha256-abc123def456..."
}
```
The fields in the metadata are:
- `fmri`: The full FMRI (Fault Management Resource Identifier) of the obsoleted package
- `status`: Always "obsolete" for obsoleted packages
- `obsolescence_date`: The date when the package was marked as obsoleted
- `deprecation_message`: Optional message explaining why the package was obsoleted
- `obsoleted_by`: Optional list of FMRIs that replace this package
- `metadata_version`: Version of the metadata schema (currently 1)
- `content_hash`: Hash of the original manifest content for integrity verification
## CLI Commands
The following CLI commands are available for managing obsoleted packages:
### Mark a Package as Obsoleted
```bash
pkg6repo obsolete-package -s <repository> -p <publisher> -f <fmri> [-m <message>] [-r <replacement-fmri>...]
```
This command:
1. Moves the package from the main repository to the obsoleted directory
2. Creates metadata for the obsoleted package
3. Removes the package from the catalog
4. Rebuilds the repository metadata
**Example:**
```bash
# Mark a package as obsoleted with a deprecation message and replacement package
pkg6repo obsolete-package -s /path/to/repo -p openindiana.org -f "pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3" \
-m "This package is deprecated. Use library/perl-5/postgres-dbi instead." \
-r "pkg://openindiana.org/library/perl-5/postgres-dbi@3.0.0"
```
### List Obsoleted Packages
```bash
pkg6repo list-obsoleted -s <repository> -p <publisher> [-F <format>] [-H] [--page <page>] [--page-size <page_size>]
```
This command lists obsoleted packages for a publisher with optional pagination. The output format can be:
- `table` (default): Tabular format with columns for name, version, and publisher
- `json`: JSON format
- `tsv`: Tab-separated values
Pagination parameters:
- `--page`: Page number (1-based, defaults to 1)
- `--page-size`: Number of packages per page (defaults to 100, use 0 for all packages)
The output includes pagination information (current page, total pages, total count) in all formats.
**Example:**
```bash
# List all obsoleted packages for a publisher in JSON format
pkg6repo list-obsoleted -s /path/to/repo -p openindiana.org -F json
# List all obsoleted packages for a publisher in table format without headers
pkg6repo list-obsoleted -s /path/to/repo -p openindiana.org -H
# List obsoleted packages with pagination (page 2, 20 packages per page)
pkg6repo list-obsoleted -s /path/to/repo -p openindiana.org --page 2 --page-size 20
# List all obsoleted packages in a single page
pkg6repo list-obsoleted -s /path/to/repo -p openindiana.org --page-size 0
```
### Search Obsoleted Packages
```bash
pkg6repo search-obsoleted -s <repository> -p <publisher> -q <pattern> [-F <format>] [-H] [-n <limit>]
```
This command searches for obsoleted packages that match a pattern. The pattern can be a simple substring or a regular expression.
**Example:**
```bash
# Search for obsoleted packages containing "perl" in the name or FMRI
pkg6repo search-obsoleted -s /path/to/repo -p openindiana.org -q "perl"
# Search with a regular expression and limit results to 10
pkg6repo search-obsoleted -s /path/to/repo -p openindiana.org -q "^library/.*" -n 10
```
### Show Obsoleted Package Details
```bash
pkg6repo show-obsoleted -s <repository> -p <publisher> -f <fmri> [-F <format>]
```
This command shows detailed information about an obsoleted package, including:
- FMRI
- Status
- Obsolescence date
- Deprecation message (if any)
- Replacement packages (if any)
- Metadata version
- Content hash
**Example:**
```bash
# Show details of an obsoleted package in JSON format
pkg6repo show-obsoleted -s /path/to/repo -p openindiana.org \
-f "pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3" -F json
```
### Restore an Obsoleted Package
```bash
pkg6repo restore-obsoleted -s <repository> -p <publisher> -f <fmri> [--no-rebuild]
```
This command restores an obsoleted package to the main repository:
1. Retrieves the original manifest from the obsoleted package
2. Creates a transaction in the main repository
3. Adds the package to the transaction
4. Commits the transaction
5. Removes the obsoleted package from the obsoleted packages directory
6. Rebuilds the catalog (unless `--no-rebuild` is specified)
**Example:**
```bash
# Restore an obsoleted package to the main repository
pkg6repo restore-obsoleted -s /path/to/repo -p openindiana.org \
-f "pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3"
```
### Export Obsoleted Packages
```bash
pkg6repo export-obsoleted -s <repository> -p <publisher> -o <output-file> [-q <pattern>]
```
This command exports obsoleted packages to a JSON file that can be imported into another repository.
**Example:**
```bash
# Export all obsoleted packages for a publisher
pkg6repo export-obsoleted -s /path/to/repo -p openindiana.org -o /path/to/export.json
# Export only obsoleted packages matching a pattern
pkg6repo export-obsoleted -s /path/to/repo -p openindiana.org -o /path/to/export.json -q "perl"
```
### Import Obsoleted Packages
```bash
pkg6repo import-obsoleted -s <repository> -i <input-file> [-p <publisher>]
```
This command imports obsoleted packages from a JSON file created by `export-obsoleted`.
**Example:**
```bash
# Import obsoleted packages from a file
pkg6repo import-obsoleted -s /path/to/repo -i /path/to/export.json
# Import obsoleted packages and override the publisher
pkg6repo import-obsoleted -s /path/to/repo -i /path/to/export.json -p new-publisher
```
## Importing Obsoleted Packages
When importing packages from a pkg5 repository, packages with the `pkg.obsolete` attribute are automatically detected and stored in the obsoleted directory instead of the main repository. This ensures that obsoleted packages are properly handled during import.
## API
The following classes and methods are available for programmatically managing obsoleted packages:
### ObsoletedPackageManager
This class manages obsoleted packages in the repository:
```
pub struct ObsoletedPackageManager {
base_path: PathBuf,
}
impl ObsoletedPackageManager {
// Create a new ObsoletedPackageManager
pub fn new<P: AsRef<Path>>(repo_path: P) -> Self;
// Initialize the obsoleted packages directory structure
pub fn init(&self) -> Result<()>;
// Store an obsoleted package
pub fn store_obsoleted_package(
&self,
publisher: &str,
fmri: &Fmri,
manifest_content: &str,
obsoleted_by: Option<Vec<String>>,
deprecation_message: Option<String>,
) -> Result<PathBuf>;
// Check if a package is obsoleted
pub fn is_obsoleted(&self, publisher: &str, fmri: &Fmri) -> bool;
// Get metadata for an obsoleted package
pub fn get_obsoleted_package_metadata(
&self,
publisher: &str,
fmri: &Fmri,
) -> Result<Option<ObsoletedPackageMetadata>>;
// List all obsoleted packages for a publisher
pub fn list_obsoleted_packages(&self, publisher: &str) -> Result<Vec<Fmri>>;
// Search for obsoleted packages by pattern
pub fn search_obsoleted_packages(
&self,
publisher: &str,
pattern: &str,
) -> Result<Vec<Fmri>>;
// Get and remove an obsoleted package
pub fn get_and_remove_obsoleted_package(
&self,
publisher: &str,
fmri: &Fmri,
) -> Result<String>;
// Export obsoleted packages to a file
pub fn export_obsoleted_packages(
&self,
publisher: &str,
pattern: Option<&str>,
output_file: &Path,
) -> Result<usize>;
// Import obsoleted packages from a file
pub fn import_obsoleted_packages(
&self,
input_file: &Path,
override_publisher: Option<&str>,
) -> Result<usize>;
}
```
### ObsoletedPackageMetadata
This struct represents metadata for an obsoleted package:
```
pub struct ObsoletedPackageMetadata {
pub fmri: String,
pub status: String,
pub obsolescence_date: String,
pub deprecation_message: Option<String>,
pub obsoleted_by: Option<Vec<String>>,
pub metadata_version: u32,
pub content_hash: String,
}
```
## Integration with Repository Operations
The obsoleted package system is integrated with the following repository operations:
1. **Package Listing**: Obsoleted packages are excluded from regular package listings
2. **Catalog Building**: Obsoleted packages are excluded from the catalog
3. **Search**: Obsoleted packages are excluded from search results
This ensures that obsoleted packages don't clutter the repository and are properly managed.
## Best Practices for Managing Obsoleted Packages
Here are some best practices for managing obsoleted packages:
### When to Mark a Package as Obsoleted
- **Package is no longer maintained**: When a package is no longer being maintained or updated
- **Package has been replaced**: When a package has been replaced by a newer version or a different package
- **Package is deprecated**: When a package is deprecated and should not be used in new installations
- **Package has security vulnerabilities**: When a package has security vulnerabilities and should not be used
### Providing Useful Metadata
- **Always include a deprecation message**: Explain why the package is obsoleted and what users should do instead
- **Specify replacement packages**: If the package has been replaced, specify the replacement package(s)
- **Be specific about versions**: If only certain versions are obsoleted, be clear about which ones
### Managing Large Numbers of Obsoleted Packages
- **Use batch operations**: Use the export/import commands to manage large numbers of obsoleted packages
- **Use search to find related packages**: Use the search command to find related packages that might also need to be obsoleted
- **Organize by publisher**: Keep obsoleted packages organized by publisher
### Repository Maintenance
- **Regularly clean up obsoleted packages**: Remove obsoleted packages that are no longer needed
- **Export obsoleted packages before repository cleanup**: Export obsoleted packages before cleaning up a repository
- **Rebuild catalogs after bulk operations**: Rebuild catalogs after bulk operations to ensure consistency
## Troubleshooting
Here are solutions to common issues when working with obsoleted packages:
### Package Not Found in Obsoleted Directory
**Issue**: A package that was marked as obsoleted cannot be found in the obsoleted directory.
**Solution**:
1. Check that the FMRI is correct, including the version and timestamp
2. Verify that the publisher name is correct
3. Use the `search-obsoleted` command with a broader pattern to find similar packages
4. Check the repository logs for any errors during the obsolete operation
### Errors During Import/Export
**Issue**: Errors occur when importing or exporting obsoleted packages.
**Solution**:
1. Ensure the input/output file paths are correct and writable
2. Check that the repository exists and is accessible
3. Verify that the publisher exists in the repository
4. For import errors, check that the JSON file is valid and has the correct format
### Catalog Issues After Restoring Packages
**Issue**: Catalog issues after restoring obsoleted packages to the main repository.
**Solution**:
1. Rebuild the catalog manually using `pkg6repo rebuild`
2. Check for any errors during the rebuild process
3. Verify that the package was properly restored to the main repository
4. Check for any conflicts with existing packages
### Performance Issues with Large Repositories
**Issue**: Performance issues when working with large repositories with many obsoleted packages.
**Solution**:
1. Use the search command with specific patterns to limit the number of packages processed
2. Use pagination when listing or searching for obsoleted packages
3. Export obsoleted packages to separate files by category or pattern
4. Consider using a more powerful machine for repository operations
## Workflow Diagram
Here's a simplified workflow for managing obsoleted packages:
```
+-------------------+
| Active Repository |
+-------------------+
|
| obsolete-package
v
+-------------------+
| Obsoleted Storage |
+-------------------+
|
| (manage)
v
+------------------------------------------+
| |
+-----------+-----------+ +-----------+-----------+
| list-obsoleted | | search-obsoleted |
| show-obsoleted | | export-obsoleted |
+-----------------------+ +-----------------------+
| |
v v
+-----------------------+ +-----------------------+
| restore-obsoleted | | import-obsoleted |
+-----------------------+ +-----------------------+
| |
v v
+-----------------------+ +-----------------------+
| Back to Active Repo | | Different Repository |
+-----------------------+ +-----------------------+
```
This diagram illustrates the flow of packages between the active repository and the obsoleted storage, as well as the various commands used to manage obsoleted packages.

View file

@ -36,3 +36,9 @@ diff-struct = "0.5.3"
chrono = "0.4.41" chrono = "0.4.41"
tempfile = "3.20.0" tempfile = "3.20.0"
walkdir = "2.4.0" walkdir = "2.4.0"
redb = "1.5.0"
bincode = "1.3.3"
[features]
default = ["redb-index"]
redb-index = [] # Enable redb-based index for obsoleted packages

View file

@ -221,6 +221,8 @@ pub struct FileBackend {
/// Catalog manager for handling catalog operations /// Catalog manager for handling catalog operations
/// Uses RefCell for interior mutability to allow mutation through immutable references /// Uses RefCell for interior mutability to allow mutation through immutable references
catalog_manager: Option<std::cell::RefCell<crate::repository::catalog::CatalogManager>>, catalog_manager: Option<std::cell::RefCell<crate::repository::catalog::CatalogManager>>,
/// Manager for obsoleted packages
obsoleted_manager: Option<std::cell::RefCell<crate::repository::obsoleted::ObsoletedPackageManager>>,
} }
/// Format a SystemTime as an ISO 8601 timestamp string /// Format a SystemTime as an ISO 8601 timestamp string
@ -616,6 +618,7 @@ impl ReadableRepository for FileBackend {
path: path.to_path_buf(), path: path.to_path_buf(),
config, config,
catalog_manager: None, catalog_manager: None,
obsoleted_manager: None,
}) })
} }
@ -1295,6 +1298,7 @@ impl WritableRepository for FileBackend {
path: path.to_path_buf(), path: path.to_path_buf(),
config, config,
catalog_manager: None, catalog_manager: None,
obsoleted_manager: None,
}; };
// Create the repository directories // Create the repository directories
@ -1582,19 +1586,27 @@ impl FileBackend {
} }
// If the publisher is not set in the FMRI, use the current publisher // If the publisher is not set in the FMRI, use the current publisher
if parsed_fmri.publisher.is_none() { let final_fmri = if parsed_fmri.publisher.is_none() {
let mut fmri_with_publisher = parsed_fmri.clone(); let mut fmri_with_publisher = parsed_fmri.clone();
fmri_with_publisher.publisher = fmri_with_publisher.publisher =
Some(publisher.to_string()); Some(publisher.to_string());
fmri_with_publisher
// Create a PackageInfo struct and add it to the list
packages.push(PackageInfo {
fmri: fmri_with_publisher,
});
} else { } else {
parsed_fmri.clone()
};
// Check if the package is obsoleted
let is_obsoleted = if let Some(obsoleted_manager) = &self.obsoleted_manager {
obsoleted_manager.borrow().is_obsoleted(publisher, &final_fmri)
} else {
false
};
// Only add the package if it's not obsoleted
if !is_obsoleted {
// Create a PackageInfo struct and add it to the list // Create a PackageInfo struct and add it to the list
packages.push(PackageInfo { packages.push(PackageInfo {
fmri: parsed_fmri.clone(), fmri: final_fmri,
}); });
} }
@ -1635,6 +1647,7 @@ impl FileBackend {
fs::create_dir_all(self.path.join("index"))?; fs::create_dir_all(self.path.join("index"))?;
fs::create_dir_all(self.path.join("pkg"))?; fs::create_dir_all(self.path.join("pkg"))?;
fs::create_dir_all(self.path.join("trans"))?; fs::create_dir_all(self.path.join("trans"))?;
fs::create_dir_all(self.path.join("obsoleted"))?;
Ok(()) Ok(())
} }
@ -1961,6 +1974,23 @@ impl FileBackend {
Ok(self.catalog_manager.as_ref().unwrap().borrow_mut()) Ok(self.catalog_manager.as_ref().unwrap().borrow_mut())
} }
/// Get or initialize the obsoleted package manager
///
/// This method returns a mutable reference to the obsoleted package manager.
/// It uses interior mutability with RefCell to allow mutation through an immutable reference.
pub fn get_obsoleted_manager(
&mut self,
) -> Result<std::cell::RefMut<crate::repository::obsoleted::ObsoletedPackageManager>> {
if self.obsoleted_manager.is_none() {
let manager = crate::repository::obsoleted::ObsoletedPackageManager::new(&self.path);
let refcell = std::cell::RefCell::new(manager);
self.obsoleted_manager = Some(refcell);
}
// This is safe because we just checked that obsoleted_manager is Some
Ok(self.obsoleted_manager.as_ref().unwrap().borrow_mut())
}
/// URL encode a string for use in a filename /// URL encode a string for use in a filename
fn url_encode(s: &str) -> String { fn url_encode(s: &str) -> String {
let mut result = String::new(); let mut result = String::new();

View file

@ -155,8 +155,52 @@ impl From<StripPrefixError> for RepositoryError {
} }
} }
// Implement From for redb error types
impl From<redb::Error> for RepositoryError {
fn from(err: redb::Error) -> Self {
RepositoryError::Other(format!("Database error: {}", err))
}
}
impl From<redb::DatabaseError> for RepositoryError {
fn from(err: redb::DatabaseError) -> Self {
RepositoryError::Other(format!("Database error: {}", err))
}
}
impl From<redb::TransactionError> for RepositoryError {
fn from(err: redb::TransactionError) -> Self {
RepositoryError::Other(format!("Transaction error: {}", err))
}
}
impl From<redb::TableError> for RepositoryError {
fn from(err: redb::TableError) -> Self {
RepositoryError::Other(format!("Table error: {}", err))
}
}
impl From<redb::StorageError> for RepositoryError {
fn from(err: redb::StorageError) -> Self {
RepositoryError::Other(format!("Storage error: {}", err))
}
}
impl From<redb::CommitError> for RepositoryError {
fn from(err: redb::CommitError) -> Self {
RepositoryError::Other(format!("Commit error: {}", err))
}
}
impl From<bincode::Error> for RepositoryError {
fn from(err: bincode::Error) -> Self {
RepositoryError::Other(format!("Serialization error: {}", err))
}
}
mod catalog; mod catalog;
mod file_backend; mod file_backend;
mod obsoleted;
mod rest_backend; mod rest_backend;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -167,6 +211,7 @@ pub use catalog::{
CatalogAttrs, CatalogError, CatalogManager, CatalogOperationType, CatalogPart, UpdateLog, CatalogAttrs, CatalogError, CatalogManager, CatalogOperationType, CatalogPart, UpdateLog,
}; };
pub use file_backend::FileBackend; pub use file_backend::FileBackend;
pub use obsoleted::{ObsoletedPackageManager, ObsoletedPackageMetadata};
pub use rest_backend::RestBackend; pub use rest_backend::RestBackend;
/// Repository configuration filename /// Repository configuration filename

File diff suppressed because it is too large Load diff

View file

@ -389,4 +389,150 @@ mod e2e_tests {
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
} }
#[test]
fn test_e2e_obsoleted_packages() {
// Run the setup script to prepare the test environment
let (prototype_dir, manifest_dir) = run_setup_script();
// Create a test directory
let test_dir = create_test_dir("e2e_obsoleted_packages");
let repo_path = test_dir.join("repo");
// Create a repository using pkg6repo
let result = run_pkg6repo(&["create", "--repo-version", "4", repo_path.to_str().unwrap()]);
assert!(
result.is_ok(),
"Failed to create repository: {:?}",
result.err()
);
// Add a publisher using pkg6repo
let result = run_pkg6repo(&["add-publisher", "-s", repo_path.to_str().unwrap(), "test"]);
assert!(
result.is_ok(),
"Failed to add publisher: {:?}",
result.err()
);
// Publish a package using pkg6dev
let manifest_path = manifest_dir.join("example.p5m");
let result = run_pkg6dev(&[
"publish",
"--manifest-path",
manifest_path.to_str().unwrap(),
"--prototype-dir",
prototype_dir.to_str().unwrap(),
"--repo-path",
repo_path.to_str().unwrap(),
]);
assert!(
result.is_ok(),
"Failed to publish package: {:?}",
result.err()
);
// Get the FMRI of the published package
let result = run_pkg6repo(&["list", "-s", repo_path.to_str().unwrap(), "-F", "json"]);
assert!(
result.is_ok(),
"Failed to list packages: {:?}",
result.err()
);
let output = result.unwrap();
let packages: serde_json::Value = serde_json::from_str(&output).expect("Failed to parse JSON output");
// The FMRI in the JSON is an object with scheme, publisher, name, and version fields
// We need to extract these fields and construct the FMRI string
let fmri_obj = &packages["packages"][0]["fmri"];
let scheme = fmri_obj["scheme"].as_str().expect("Failed to get scheme");
let publisher = fmri_obj["publisher"].as_str().expect("Failed to get publisher");
let name = fmri_obj["name"].as_str().expect("Failed to get name");
let version_obj = &fmri_obj["version"];
let release = version_obj["release"].as_str().expect("Failed to get release");
// Construct the FMRI string in the format "pkg://publisher/name@version"
let fmri = format!("{}://{}/{}", scheme, publisher, name);
// Add version if available
let fmri = if !release.is_empty() {
format!("{}@{}", fmri, release)
} else {
fmri
};
// Mark the package as obsoleted
let result = run_pkg6repo(&[
"obsolete-package",
"-s", repo_path.to_str().unwrap(),
"-p", "test",
"-f", &fmri,
"-m", "This package is obsoleted for testing purposes",
"-r", "pkg://test/example2@1.0"
]);
assert!(
result.is_ok(),
"Failed to mark package as obsoleted: {:?}",
result.err()
);
// Verify the package is no longer in the main repository
let result = run_pkg6repo(&["list", "-s", repo_path.to_str().unwrap()]);
assert!(
result.is_ok(),
"Failed to list packages: {:?}",
result.err()
);
let output = result.unwrap();
assert!(
!output.contains("example"),
"Package still found in repository after being marked as obsoleted"
);
// List obsoleted packages
let result = run_pkg6repo(&["list-obsoleted", "-s", repo_path.to_str().unwrap(), "-p", "test"]);
assert!(
result.is_ok(),
"Failed to list obsoleted packages: {:?}",
result.err()
);
let output = result.unwrap();
assert!(
output.contains("example"),
"Obsoleted package not found in obsoleted packages list"
);
// Show details of the obsoleted package
let result = run_pkg6repo(&[
"show-obsoleted",
"-s", repo_path.to_str().unwrap(),
"-p", "test",
"-f", &fmri
]);
assert!(
result.is_ok(),
"Failed to show obsoleted package details: {:?}",
result.err()
);
let output = result.unwrap();
assert!(
output.contains("Status: obsolete"),
"Package not marked as obsolete in details"
);
assert!(
output.contains("This package is obsoleted for testing purposes"),
"Deprecation message not found in details"
);
assert!(
output.contains("pkg://test/example2@1.0"),
"Replacement package not found in details"
);
// Clean up
cleanup_test_dir(&test_dir);
}
} }

View file

@ -1,4 +1,5 @@
use libips::actions::ActionError; use libips::actions::ActionError;
use libips::fmri::FmriError;
use libips::repository; use libips::repository;
use miette::Diagnostic; use miette::Diagnostic;
use thiserror::Error; use thiserror::Error;
@ -73,3 +74,10 @@ impl From<&str> for Pkg6RepoError {
Pkg6RepoError::Other(s.to_string()) Pkg6RepoError::Other(s.to_string())
} }
} }
/// Convert a FmriError to a Pkg6RepoError
impl From<FmriError> for Pkg6RepoError {
fn from(err: FmriError) -> Self {
Pkg6RepoError::Other(format!("FMRI error: {}", err))
}
}

View file

@ -3,6 +3,22 @@ mod pkg5_import;
use error::{Pkg6RepoError, Result}; use error::{Pkg6RepoError, Result};
use pkg5_import::Pkg5Importer; use pkg5_import::Pkg5Importer;
/// URL encode a string for use in a filename
fn url_encode(s: &str) -> String {
let mut result = String::new();
for c in s.chars() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
' ' => result.push('+'),
_ => {
result.push('%');
result.push_str(&format!("{:02X}", c as u8));
}
}
}
result
}
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use libips::repository::{FileBackend, ReadableRepository, RepositoryVersion, WritableRepository}; use libips::repository::{FileBackend, ReadableRepository, RepositoryVersion, WritableRepository};
use serde::Serialize; use serde::Serialize;
@ -40,6 +56,22 @@ struct SearchOutput {
results: Vec<libips::repository::PackageInfo>, 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 /// pkg6repo - Image Packaging System repository management utility
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about, long_about = None)]
@ -318,6 +350,155 @@ enum Commands {
#[clap(short = 'p', long)] #[clap(short = 'p', long)]
publisher: Option<String>, 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>,
},
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@ -1089,6 +1270,491 @@ fn main() -> Result<()> {
info!("Repository imported successfully"); info!("Repository imported successfully");
Ok(()) 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
let pkg_dir = repo.path.join("pkg").join(publisher).join(parsed_fmri.stem());
let encoded_version = url_encode(&parsed_fmri.version());
let manifest_path = pkg_dir.join(&encoded_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(())
} }
} }
} }

View file

@ -1,5 +1,6 @@
use crate::error::{Pkg6RepoError, Result}; use crate::error::{Pkg6RepoError, Result};
use libips::actions::Manifest; use libips::actions::Manifest;
use libips::fmri::Fmri;
use libips::repository::{FileBackend, ReadableRepository, WritableRepository}; use libips::repository::{FileBackend, ReadableRepository, WritableRepository};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{Read, Seek}; use std::io::{Read, Seek};
@ -220,14 +221,21 @@ impl Pkg5Importer {
} }
} }
// Import packages // Import packages and get counts
self.import_packages(&source_path, &mut dest_repo, publisher_to_import)?; let (regular_count, obsoleted_count) = self.import_packages(&source_path, &mut dest_repo, publisher_to_import)?;
let total_count = regular_count + obsoleted_count;
// Rebuild catalog and search index // Rebuild catalog and search index
info!("Rebuilding catalog and search index..."); info!("Rebuilding catalog and search index...");
dest_repo.rebuild(Some(publisher_to_import), false, false)?; dest_repo.rebuild(Some(publisher_to_import), false, false)?;
// Report final statistics
info!("Import completed successfully"); info!("Import completed successfully");
info!("Import summary:");
info!(" Total packages processed: {}", total_count);
info!(" Regular packages imported: {}", regular_count);
info!(" Obsoleted packages stored: {}", obsoleted_count);
Ok(()) Ok(())
} }
@ -259,12 +267,14 @@ impl Pkg5Importer {
} }
/// Imports packages from the source repository /// Imports packages from the source repository
///
/// Returns a tuple of (regular_package_count, obsoleted_package_count)
fn import_packages( fn import_packages(
&self, &self,
source_path: &Path, source_path: &Path,
dest_repo: &mut FileBackend, dest_repo: &mut FileBackend,
publisher: &str, publisher: &str,
) -> Result<()> { ) -> Result<(usize, usize)> {
let pkg_dir = source_path.join("publisher").join(publisher).join("pkg"); let pkg_dir = source_path.join("publisher").join(publisher).join("pkg");
if !pkg_dir.exists() || !pkg_dir.is_dir() { if !pkg_dir.exists() || !pkg_dir.is_dir() {
@ -288,7 +298,8 @@ impl Pkg5Importer {
); );
// Find package directories // Find package directories
let mut package_count = 0; let mut regular_package_count = 0;
let mut obsoleted_package_count = 0;
for pkg_entry in fs::read_dir(&pkg_dir).map_err(|e| Pkg6RepoError::IoError(e))? { for pkg_entry in fs::read_dir(&pkg_dir).map_err(|e| Pkg6RepoError::IoError(e))? {
let pkg_entry = pkg_entry.map_err(|e| Pkg6RepoError::IoError(e))?; let pkg_entry = pkg_entry.map_err(|e| Pkg6RepoError::IoError(e))?;
@ -316,7 +327,7 @@ impl Pkg5Importer {
debug!("Processing version: {}", decoded_ver_name); debug!("Processing version: {}", decoded_ver_name);
// Import this package version // Import this package version
self.import_package_version( let is_obsoleted = self.import_package_version(
source_path, source_path,
dest_repo, dest_repo,
publisher, publisher,
@ -326,17 +337,26 @@ impl Pkg5Importer {
temp_proto_dir.path(), temp_proto_dir.path(),
)?; )?;
package_count += 1; // Increment the appropriate counter
if is_obsoleted {
obsoleted_package_count += 1;
} else {
regular_package_count += 1;
}
} }
} }
} }
} }
info!("Imported {} packages", package_count); let total_package_count = regular_package_count + obsoleted_package_count;
Ok(()) info!("Imported {} packages ({} regular, {} obsoleted)",
total_package_count, regular_package_count, obsoleted_package_count);
Ok((regular_package_count, obsoleted_package_count))
} }
/// Imports a specific package version /// Imports a specific package version
///
/// Returns a boolean indicating whether the package was obsoleted
fn import_package_version( fn import_package_version(
&self, &self,
source_path: &Path, source_path: &Path,
@ -346,7 +366,7 @@ impl Pkg5Importer {
pkg_name: &str, pkg_name: &str,
_ver_name: &str, _ver_name: &str,
proto_dir: &Path, proto_dir: &Path,
) -> Result<()> { ) -> Result<bool> {
debug!("Importing package version from {}", manifest_path.display()); debug!("Importing package version from {}", manifest_path.display());
// Extract package name from FMRI // Extract package name from FMRI
@ -364,7 +384,66 @@ impl Pkg5Importer {
// Parse the manifest using parse_string // Parse the manifest using parse_string
debug!("Parsing manifest content"); debug!("Parsing manifest content");
let manifest = Manifest::parse_string(manifest_content)?; let manifest = Manifest::parse_string(manifest_content.clone())?;
// Check if this is an obsoleted package
let mut is_obsoleted = false;
let mut fmri_str = String::new();
// Extract the FMRI from the manifest
for attr in &manifest.attributes {
if attr.key == "pkg.fmri" && !attr.values.is_empty() {
fmri_str = attr.values[0].clone();
break;
}
}
// Check for pkg.obsolete attribute
for attr in &manifest.attributes {
if attr.key == "pkg.obsolete" && !attr.values.is_empty() {
if attr.values[0] == "true" {
is_obsoleted = true;
debug!("Found obsoleted package: {}", fmri_str);
break;
}
}
}
// If this is an obsoleted package, store it in the obsoleted directory
if is_obsoleted && !fmri_str.is_empty() {
debug!("Handling obsoleted package: {}", fmri_str);
// Parse the FMRI
let fmri = match Fmri::parse(&fmri_str) {
Ok(fmri) => fmri,
Err(e) => {
warn!("Failed to parse FMRI '{}': {}", fmri_str, e);
return Err(Pkg6RepoError::from(format!(
"Failed to parse FMRI '{}': {}",
fmri_str, e
)));
}
};
// Get the obsoleted package manager
let obsoleted_manager = dest_repo.get_obsoleted_manager()?;
// Store the obsoleted package
debug!("Storing obsoleted package in dedicated directory");
obsoleted_manager.store_obsoleted_package(
publisher,
&fmri,
&manifest_content,
None, // No obsoleted_by information available
None, // No deprecation message available
)?;
info!("Stored obsoleted package: {}", fmri);
return Ok(true); // Return true to indicate this was an obsoleted package
}
// For non-obsoleted packages, proceed with normal import
debug!("Processing regular (non-obsoleted) package");
// Begin a transaction // Begin a transaction
debug!("Beginning transaction"); debug!("Beginning transaction");
@ -472,7 +551,7 @@ impl Pkg5Importer {
// Commit the transaction // Commit the transaction
transaction.commit()?; transaction.commit()?;
Ok(()) Ok(false) // Return false to indicate this was a regular (non-obsoleted) package
} }
} }