mirror of
https://codeberg.org/Toasterson/ips.git
synced 2026-04-10 13:20:42 +00:00
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:
parent
88b55c4a70
commit
9b2f74c5c1
11 changed files with 3903 additions and 18 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
|
@ -144,6 +144,15 @@ version = "0.21.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
|
@ -954,6 +963,7 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
|||
name = "libips"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
"diff-struct",
|
||||
"flate2",
|
||||
|
|
@ -963,6 +973,7 @@ dependencies = [
|
|||
"object 0.23.0",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"redb",
|
||||
"regex",
|
||||
"semver",
|
||||
"serde",
|
||||
|
|
@ -1504,6 +1515,15 @@ version = "5.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "redb"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd7f82ecd6ba647a39dd1a7172b8a1cd9453c0adee6da20cb553d83a9a460fa5"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
|
|
|
|||
54
doc/Obsoletion Index.txt
Normal file
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
432
doc/obsoleted_packages.md
Normal 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.
|
||||
|
|
@ -36,3 +36,9 @@ diff-struct = "0.5.3"
|
|||
chrono = "0.4.41"
|
||||
tempfile = "3.20.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
|
||||
|
|
|
|||
|
|
@ -221,6 +221,8 @@ pub struct FileBackend {
|
|||
/// Catalog manager for handling catalog operations
|
||||
/// Uses RefCell for interior mutability to allow mutation through immutable references
|
||||
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
|
||||
|
|
@ -616,6 +618,7 @@ impl ReadableRepository for FileBackend {
|
|||
path: path.to_path_buf(),
|
||||
config,
|
||||
catalog_manager: None,
|
||||
obsoleted_manager: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1295,6 +1298,7 @@ impl WritableRepository for FileBackend {
|
|||
path: path.to_path_buf(),
|
||||
config,
|
||||
catalog_manager: None,
|
||||
obsoleted_manager: None,
|
||||
};
|
||||
|
||||
// Create the repository directories
|
||||
|
|
@ -1582,19 +1586,27 @@ impl FileBackend {
|
|||
}
|
||||
|
||||
// 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();
|
||||
fmri_with_publisher.publisher =
|
||||
Some(publisher.to_string());
|
||||
|
||||
// Create a PackageInfo struct and add it to the list
|
||||
packages.push(PackageInfo {
|
||||
fmri: fmri_with_publisher,
|
||||
});
|
||||
fmri_with_publisher
|
||||
} 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
|
||||
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("pkg"))?;
|
||||
fs::create_dir_all(self.path.join("trans"))?;
|
||||
fs::create_dir_all(self.path.join("obsoleted"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1961,6 +1974,23 @@ impl FileBackend {
|
|||
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
|
||||
fn url_encode(s: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
|
|
|||
|
|
@ -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 file_backend;
|
||||
mod obsoleted;
|
||||
mod rest_backend;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
@ -167,6 +211,7 @@ pub use catalog::{
|
|||
CatalogAttrs, CatalogError, CatalogManager, CatalogOperationType, CatalogPart, UpdateLog,
|
||||
};
|
||||
pub use file_backend::FileBackend;
|
||||
pub use obsoleted::{ObsoletedPackageManager, ObsoletedPackageMetadata};
|
||||
pub use rest_backend::RestBackend;
|
||||
|
||||
/// Repository configuration filename
|
||||
|
|
|
|||
2399
libips/src/repository/obsoleted.rs
Normal file
2399
libips/src/repository/obsoleted.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -389,4 +389,150 @@ mod e2e_tests {
|
|||
// Clean up
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
use libips::actions::ActionError;
|
||||
use libips::fmri::FmriError;
|
||||
use libips::repository;
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
|
@ -73,3 +74,10 @@ impl From<&str> for Pkg6RepoError {
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,22 @@ mod pkg5_import;
|
|||
use error::{Pkg6RepoError, Result};
|
||||
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 libips::repository::{FileBackend, ReadableRepository, RepositoryVersion, WritableRepository};
|
||||
use serde::Serialize;
|
||||
|
|
@ -40,6 +56,22 @@ struct SearchOutput {
|
|||
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)]
|
||||
|
|
@ -318,6 +350,155 @@ enum Commands {
|
|||
#[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>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
|
@ -1089,6 +1270,491 @@ fn main() -> Result<()> {
|
|||
|
||||
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
|
||||
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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::error::{Pkg6RepoError, Result};
|
||||
use libips::actions::Manifest;
|
||||
use libips::fmri::Fmri;
|
||||
use libips::repository::{FileBackend, ReadableRepository, WritableRepository};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Seek};
|
||||
|
|
@ -220,14 +221,21 @@ impl Pkg5Importer {
|
|||
}
|
||||
}
|
||||
|
||||
// Import packages
|
||||
self.import_packages(&source_path, &mut dest_repo, publisher_to_import)?;
|
||||
// Import packages and get counts
|
||||
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
|
||||
info!("Rebuilding catalog and search index...");
|
||||
dest_repo.rebuild(Some(publisher_to_import), false, false)?;
|
||||
|
||||
// Report final statistics
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
@ -259,12 +267,14 @@ impl Pkg5Importer {
|
|||
}
|
||||
|
||||
/// Imports packages from the source repository
|
||||
///
|
||||
/// Returns a tuple of (regular_package_count, obsoleted_package_count)
|
||||
fn import_packages(
|
||||
&self,
|
||||
source_path: &Path,
|
||||
dest_repo: &mut FileBackend,
|
||||
publisher: &str,
|
||||
) -> Result<()> {
|
||||
) -> Result<(usize, usize)> {
|
||||
let pkg_dir = source_path.join("publisher").join(publisher).join("pkg");
|
||||
|
||||
if !pkg_dir.exists() || !pkg_dir.is_dir() {
|
||||
|
|
@ -288,7 +298,8 @@ impl Pkg5Importer {
|
|||
);
|
||||
|
||||
// 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))? {
|
||||
let pkg_entry = pkg_entry.map_err(|e| Pkg6RepoError::IoError(e))?;
|
||||
|
|
@ -316,7 +327,7 @@ impl Pkg5Importer {
|
|||
debug!("Processing version: {}", decoded_ver_name);
|
||||
|
||||
// Import this package version
|
||||
self.import_package_version(
|
||||
let is_obsoleted = self.import_package_version(
|
||||
source_path,
|
||||
dest_repo,
|
||||
publisher,
|
||||
|
|
@ -326,17 +337,26 @@ impl Pkg5Importer {
|
|||
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);
|
||||
Ok(())
|
||||
let total_package_count = regular_package_count + obsoleted_package_count;
|
||||
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
|
||||
///
|
||||
/// Returns a boolean indicating whether the package was obsoleted
|
||||
fn import_package_version(
|
||||
&self,
|
||||
source_path: &Path,
|
||||
|
|
@ -346,7 +366,7 @@ impl Pkg5Importer {
|
|||
pkg_name: &str,
|
||||
_ver_name: &str,
|
||||
proto_dir: &Path,
|
||||
) -> Result<()> {
|
||||
) -> Result<bool> {
|
||||
debug!("Importing package version from {}", manifest_path.display());
|
||||
|
||||
// Extract package name from FMRI
|
||||
|
|
@ -364,7 +384,66 @@ impl Pkg5Importer {
|
|||
|
||||
// Parse the manifest using parse_string
|
||||
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
|
||||
debug!("Beginning transaction");
|
||||
|
|
@ -472,7 +551,7 @@ impl Pkg5Importer {
|
|||
// Commit the transaction
|
||||
transaction.commit()?;
|
||||
|
||||
Ok(())
|
||||
Ok(false) // Return false to indicate this was a regular (non-obsoleted) package
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue