Initial commit: APT-DNF Bridge workspace
- Core crate: Minimal shell-out implementation - Advanced crate: Pluggable backends and enhanced features - Main crate: Re-exports core + optional advanced features - Feature flags: Users choose complexity level - Examples: Working demonstrations of both approaches - Documentation: Clear README explaining the structure Implements the refined two-crate approach with workspace + feature flags.
This commit is contained in:
commit
06cafa0366
24 changed files with 2790 additions and 0 deletions
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Rust
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"apt-dnf-bridge-core",
|
||||
"apt-dnf-bridge",
|
||||
"apt-dnf-bridge-advanced",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/your-org/apt-dnf-bridge"
|
||||
keywords = ["apt", "dnf", "package-manager", "ostree", "bridge"]
|
||||
categories = ["os::linux-apis", "development-tools::build-utils"]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.0", features = ["process", "macros"] }
|
||||
tempfile = "3.0"
|
||||
180
README.md
Normal file
180
README.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# APT-DNF Bridge
|
||||
|
||||
A DNF-like bridge around APT for apt-ostree integration. This workspace provides a transaction-based API that makes APT work like DNF.
|
||||
|
||||
## 🎯 **Two-Crate Approach**
|
||||
|
||||
This workspace implements the refined two-crate approach with feature flags:
|
||||
|
||||
- **`apt-dnf-bridge-core`** - Minimal shell-out implementation
|
||||
- **`apt-dnf-bridge`** - Main crate that re-exports core + optional advanced features
|
||||
- **`apt-dnf-bridge-advanced`** - Pluggable backends, caching, and enhanced features
|
||||
|
||||
## 🚀 **Quick Start**
|
||||
|
||||
### Core (Minimal)
|
||||
```toml
|
||||
[dependencies]
|
||||
apt-dnf-bridge = "0.1"
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
```toml
|
||||
[dependencies]
|
||||
apt-dnf-bridge = { version = "0.1", features = ["advanced"] }
|
||||
```
|
||||
|
||||
## 📁 **Workspace Structure**
|
||||
|
||||
```
|
||||
apt-dnf-bridge-workspace/
|
||||
├── Cargo.toml # Workspace root
|
||||
├── apt-dnf-bridge-core/ # Minimal implementation
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
│ ├── lib.rs
|
||||
│ ├── command.rs
|
||||
│ ├── error.rs
|
||||
│ ├── package.rs
|
||||
│ ├── repository.rs
|
||||
│ └── transaction.rs
|
||||
├── apt-dnf-bridge/ # Main public crate
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
│ └── lib.rs
|
||||
├── apt-dnf-bridge-advanced/ # Advanced features
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
│ ├── lib.rs
|
||||
│ ├── backend.rs
|
||||
│ ├── package_v2.rs
|
||||
│ ├── transaction_v2.rs
|
||||
│ └── backend/
|
||||
│ ├── shell_backend.rs
|
||||
│ ├── mock_backend.rs
|
||||
│ └── libapt_backend.rs
|
||||
└── examples/ # Shared examples
|
||||
├── basic_usage.rs
|
||||
├── package_query.rs
|
||||
├── atomicity_notes.rs
|
||||
└── backend_selection.rs
|
||||
```
|
||||
|
||||
## 🔧 **Usage Examples**
|
||||
|
||||
### Basic Usage (Core Only)
|
||||
```rust
|
||||
use apt_dnf_bridge::{Transaction, Package, Repository};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a transaction
|
||||
let mut tx = Transaction::new();
|
||||
|
||||
// Add packages to install
|
||||
let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64");
|
||||
tx.add_install(vim).await?;
|
||||
|
||||
// Resolve dependencies (APT handles automatically)
|
||||
tx.resolve().await?;
|
||||
|
||||
// Commit the transaction
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Usage (With Backends)
|
||||
```rust
|
||||
use apt_dnf_bridge::{TransactionV2, PackageDatabaseV2, BackendFactory};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create transaction with auto-detected backend
|
||||
let mut tx = TransactionV2::new().await?;
|
||||
|
||||
// Add packages
|
||||
let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64");
|
||||
tx.add_install(vim).await?;
|
||||
|
||||
// Resolve and commit
|
||||
tx.resolve().await?;
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 🏗️ **Building and Testing**
|
||||
|
||||
### Build All Crates
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
### Test Core Features
|
||||
```bash
|
||||
cargo test -p apt-dnf-bridge-core
|
||||
```
|
||||
|
||||
### Test Advanced Features
|
||||
```bash
|
||||
cargo test -p apt-dnf-bridge-advanced
|
||||
```
|
||||
|
||||
### Run Examples
|
||||
```bash
|
||||
# Core examples
|
||||
cargo run --example basic_usage
|
||||
cargo run --example package_query
|
||||
|
||||
# Advanced examples (requires advanced feature)
|
||||
cargo run --example backend_selection --features advanced
|
||||
```
|
||||
|
||||
## 🎯 **Feature Flags**
|
||||
|
||||
### Core Features (Default)
|
||||
- Shell-out APT commands
|
||||
- Basic transaction model
|
||||
- Package querying
|
||||
- Repository management
|
||||
- Error handling
|
||||
|
||||
### Advanced Features (`advanced` feature)
|
||||
- Pluggable backends (shell, mock, libapt)
|
||||
- Caching system
|
||||
- Enhanced error handling
|
||||
- Backend statistics
|
||||
- Mock backend for testing
|
||||
|
||||
## 🔄 **Migration Guide**
|
||||
|
||||
### From Single Crate to Workspace
|
||||
1. **Core users**: No changes needed - same API
|
||||
2. **Advanced users**: Add `features = ["advanced"]` to Cargo.toml
|
||||
3. **New users**: Start with core, add advanced when needed
|
||||
|
||||
### API Compatibility
|
||||
- Core API remains unchanged
|
||||
- Advanced API available via feature flag
|
||||
- Gradual adoption path supported
|
||||
|
||||
## 📚 **Documentation**
|
||||
|
||||
- **Core API**: Focus on simplicity and reliability
|
||||
- **Advanced API**: Focus on power and flexibility
|
||||
- **Examples**: Demonstrate both core and advanced usage
|
||||
- **Migration**: Guide for moving between feature levels
|
||||
|
||||
## 🚀 **Benefits of This Approach**
|
||||
|
||||
1. **Single Crate** - No confusion about which to use
|
||||
2. **Feature Flags** - Users choose complexity level
|
||||
3. **Workspace** - Easy version coordination
|
||||
4. **Re-exports** - Clean API surface
|
||||
5. **Gradual Adoption** - Start simple, add complexity when needed
|
||||
6. **Clear Documentation** - One README with feature explanations
|
||||
|
||||
This approach gives us all the benefits of the two-crate strategy with much better UX and maintenance.
|
||||
19
apt-dnf-bridge-advanced/Cargo.toml
Normal file
19
apt-dnf-bridge-advanced/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "apt-dnf-bridge-advanced"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Advanced features for apt-dnf-bridge: pluggable backends, caching, and more"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[dependencies]
|
||||
apt-dnf-bridge-core = { path = "../apt-dnf-bridge-core" }
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
serde.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
[features]
|
||||
libapt-backend = []
|
||||
182
apt-dnf-bridge-advanced/src/backend.rs
Normal file
182
apt-dnf-bridge-advanced/src/backend.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
//! Pluggable backend trait for different APT implementations.
|
||||
|
||||
use apt_dnf_bridge_core::{Package, Result, Operation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Resolution result from a backend.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Resolution {
|
||||
/// Whether the transaction can be resolved successfully.
|
||||
pub resolvable: bool,
|
||||
/// Packages that would be installed.
|
||||
pub to_install: Vec<Package>,
|
||||
/// Packages that would be removed.
|
||||
pub to_remove: Vec<Package>,
|
||||
/// Packages that would be upgraded.
|
||||
pub to_upgrade: Vec<Package>,
|
||||
/// Dependencies that would be installed.
|
||||
pub dependencies: Vec<Package>,
|
||||
/// Any conflicts or issues found.
|
||||
pub conflicts: Vec<String>,
|
||||
/// Human-readable summary of the resolution.
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
/// Repository information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RepositoryInfo {
|
||||
/// Repository name.
|
||||
pub name: String,
|
||||
/// Repository URL.
|
||||
pub url: String,
|
||||
/// Repository components.
|
||||
pub components: Vec<String>,
|
||||
/// Whether the repository is enabled.
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Package query options.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct QueryOptions {
|
||||
/// Whether to use caching.
|
||||
pub use_cache: bool,
|
||||
/// Whether to enable logging.
|
||||
pub enable_logging: bool,
|
||||
/// Maximum number of results to return.
|
||||
pub max_results: Option<usize>,
|
||||
}
|
||||
|
||||
/// Backend trait for different APT implementations.
|
||||
///
|
||||
/// This trait allows the crate to support multiple backends:
|
||||
/// - ShellBackend: Uses shell-out to apt commands (simple, reliable)
|
||||
/// - LibAptBackend: Uses libapt-pkg bindings (more powerful, complex)
|
||||
/// - MockBackend: For testing (no real APT calls)
|
||||
#[async_trait::async_trait]
|
||||
pub trait AptBackend: Send + Sync {
|
||||
/// Get the name of this backend.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Get the version of this backend.
|
||||
fn version(&self) -> &'static str;
|
||||
|
||||
/// Check if this backend is available on the current system.
|
||||
async fn is_available(&self) -> Result<bool>;
|
||||
|
||||
/// Resolve a transaction to check if it can be executed.
|
||||
async fn resolve(&self, operations: &[Operation]) -> Result<Resolution>;
|
||||
|
||||
/// Commit a transaction by executing the operations.
|
||||
async fn commit(&self, operations: &[Operation]) -> Result<()>;
|
||||
|
||||
/// Search for packages by pattern.
|
||||
async fn search_packages(&mut self, pattern: &str, options: &QueryOptions) -> Result<Vec<Package>>;
|
||||
|
||||
/// Get detailed information about a specific package.
|
||||
async fn get_package_info(&mut self, name: &str, options: &QueryOptions) -> Result<Option<Package>>;
|
||||
|
||||
/// Check if a package is installed.
|
||||
async fn is_package_installed(&self, name: &str) -> Result<bool>;
|
||||
|
||||
/// Get all installed packages.
|
||||
async fn get_installed_packages(&self, options: &QueryOptions) -> Result<Vec<Package>>;
|
||||
|
||||
/// Add a repository.
|
||||
async fn add_repository(&self, repo: &RepositoryInfo) -> Result<()>;
|
||||
|
||||
/// Remove a repository.
|
||||
async fn remove_repository(&self, name: &str) -> Result<()>;
|
||||
|
||||
/// List all repositories.
|
||||
async fn list_repositories(&self) -> Result<Vec<RepositoryInfo>>;
|
||||
|
||||
/// Update package cache.
|
||||
async fn update_cache(&self) -> Result<()>;
|
||||
|
||||
/// Get backend-specific statistics.
|
||||
async fn get_stats(&self) -> Result<BackendStats>;
|
||||
}
|
||||
|
||||
/// Backend statistics.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BackendStats {
|
||||
/// Number of commands executed.
|
||||
pub commands_executed: u64,
|
||||
/// Number of packages queried.
|
||||
pub packages_queried: u64,
|
||||
/// Number of transactions committed.
|
||||
pub transactions_committed: u64,
|
||||
/// Cache hit rate (0.0 to 1.0).
|
||||
pub cache_hit_rate: f64,
|
||||
/// Average command execution time in milliseconds.
|
||||
pub avg_command_time_ms: f64,
|
||||
}
|
||||
|
||||
/// Backend configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BackendConfig {
|
||||
/// Whether to enable logging.
|
||||
pub enable_logging: bool,
|
||||
/// Whether to use caching.
|
||||
pub use_cache: bool,
|
||||
/// Maximum cache size.
|
||||
pub max_cache_size: usize,
|
||||
/// Timeout for commands in seconds.
|
||||
pub command_timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl Default for BackendConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enable_logging: false,
|
||||
use_cache: true,
|
||||
max_cache_size: 1000,
|
||||
command_timeout: Some(300), // 5 minutes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Backend factory for creating different backend implementations.
|
||||
pub struct BackendFactory;
|
||||
|
||||
impl BackendFactory {
|
||||
/// Create a shell backend (default, most reliable).
|
||||
pub fn create_shell_backend(config: BackendConfig) -> Result<Box<dyn AptBackend>> {
|
||||
Ok(Box::new(ShellBackend::new(config)))
|
||||
}
|
||||
|
||||
/// Create a libapt backend (if available).
|
||||
#[cfg(feature = "libapt-backend")]
|
||||
pub fn create_libapt_backend(config: BackendConfig) -> Result<Box<dyn AptBackend>> {
|
||||
Ok(Box::new(LibAptBackend::new(config)))
|
||||
}
|
||||
|
||||
/// Create a mock backend for testing.
|
||||
pub fn create_mock_backend(config: BackendConfig) -> Result<Box<dyn AptBackend>> {
|
||||
Ok(Box::new(MockBackend::new(config)))
|
||||
}
|
||||
|
||||
/// Auto-detect the best available backend.
|
||||
pub async fn auto_detect(config: BackendConfig) -> Result<Box<dyn AptBackend>> {
|
||||
// Try libapt first if available
|
||||
#[cfg(feature = "libapt-backend")]
|
||||
{
|
||||
let libapt = Self::create_libapt_backend(config.clone())?;
|
||||
if libapt.is_available().await? {
|
||||
return Ok(libapt);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to shell backend
|
||||
Self::create_shell_backend(config)
|
||||
}
|
||||
}
|
||||
|
||||
// Backend implementations
|
||||
mod shell_backend;
|
||||
mod libapt_backend;
|
||||
mod mock_backend;
|
||||
|
||||
pub use shell_backend::ShellBackend;
|
||||
pub use libapt_backend::LibAptBackend;
|
||||
pub use mock_backend::MockBackend;
|
||||
126
apt-dnf-bridge-advanced/src/backend/libapt_backend.rs
Normal file
126
apt-dnf-bridge-advanced/src/backend/libapt_backend.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
//! LibApt backend implementation using libapt-pkg bindings.
|
||||
//!
|
||||
//! This is a placeholder implementation that would use apt-rs or direct libapt-pkg bindings
|
||||
//! when available. Currently, it falls back to shell commands.
|
||||
|
||||
use crate::backend::{AptBackend, BackendConfig, BackendStats, QueryOptions, RepositoryInfo, Resolution};
|
||||
use apt_dnf_bridge_core::{AptError, Operation, Package, Result};
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// LibApt backend that uses libapt-pkg bindings.
|
||||
///
|
||||
/// This backend provides more accurate dependency resolution and better performance
|
||||
/// than the shell backend, but requires libapt-pkg to be available.
|
||||
pub struct LibAptBackend {
|
||||
config: BackendConfig,
|
||||
stats: BackendStats,
|
||||
// TODO: Add libapt-pkg specific fields when implementing
|
||||
}
|
||||
|
||||
impl LibAptBackend {
|
||||
/// Create a new libapt backend.
|
||||
pub fn new(config: BackendConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
stats: BackendStats {
|
||||
commands_executed: 0,
|
||||
packages_queried: 0,
|
||||
transactions_committed: 0,
|
||||
cache_hit_rate: 0.0,
|
||||
avg_command_time_ms: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AptBackend for LibAptBackend {
|
||||
fn name(&self) -> &'static str {
|
||||
"libapt"
|
||||
}
|
||||
|
||||
fn version(&self) -> &'static str {
|
||||
"0.1.0-placeholder"
|
||||
}
|
||||
|
||||
async fn is_available(&self) -> Result<bool> {
|
||||
// TODO: Check if libapt-pkg is available
|
||||
// For now, always return false to indicate it's not implemented
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn resolve(&self, _operations: &[Operation]) -> Result<Resolution> {
|
||||
// TODO: Use libapt-pkg for proper dependency resolution
|
||||
// For now, return a placeholder response
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn commit(&self, _operations: &[Operation]) -> Result<()> {
|
||||
// TODO: Use libapt-pkg for transaction execution
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn search_packages(&mut self, _pattern: &str, _options: &QueryOptions) -> Result<Vec<Package>> {
|
||||
// TODO: Use libapt-pkg for package searching
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn get_package_info(&mut self, _name: &str, _options: &QueryOptions) -> Result<Option<Package>> {
|
||||
// TODO: Use libapt-pkg for package info
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn is_package_installed(&self, _name: &str) -> Result<bool> {
|
||||
// TODO: Use libapt-pkg for installation check
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn get_installed_packages(&self, _options: &QueryOptions) -> Result<Vec<Package>> {
|
||||
// TODO: Use libapt-pkg for installed packages
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn add_repository(&self, _repo: &RepositoryInfo) -> Result<()> {
|
||||
// TODO: Use libapt-pkg for repository management
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn remove_repository(&self, _name: &str) -> Result<()> {
|
||||
// TODO: Use libapt-pkg for repository management
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn list_repositories(&self) -> Result<Vec<RepositoryInfo>> {
|
||||
// TODO: Use libapt-pkg for repository listing
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn update_cache(&self) -> Result<()> {
|
||||
// TODO: Use libapt-pkg for cache updates
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"LibApt backend not yet implemented. Use shell backend instead."
|
||||
)))
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> Result<BackendStats> {
|
||||
Ok(self.stats.clone())
|
||||
}
|
||||
}
|
||||
176
apt-dnf-bridge-advanced/src/backend/mock_backend.rs
Normal file
176
apt-dnf-bridge-advanced/src/backend/mock_backend.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
//! Mock backend implementation for testing.
|
||||
|
||||
use crate::backend::{AptBackend, BackendConfig, BackendStats, QueryOptions, RepositoryInfo, Resolution};
|
||||
use apt_dnf_bridge_core::{Operation, Package, Result};
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Mock backend for testing that doesn't make real APT calls.
|
||||
pub struct MockBackend {
|
||||
config: BackendConfig,
|
||||
stats: BackendStats,
|
||||
packages: HashMap<String, Package>,
|
||||
repositories: HashMap<String, RepositoryInfo>,
|
||||
}
|
||||
|
||||
impl MockBackend {
|
||||
/// Create a new mock backend.
|
||||
pub fn new(config: BackendConfig) -> Self {
|
||||
let mut packages = HashMap::new();
|
||||
|
||||
// Add some mock packages
|
||||
packages.insert("vim".to_string(), Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64"));
|
||||
packages.insert("nano".to_string(), Package::new("nano", "2.9.3-2", "amd64"));
|
||||
packages.insert("emacs".to_string(), Package::new("emacs", "1:27.1+1-3ubuntu2", "amd64"));
|
||||
|
||||
Self {
|
||||
config,
|
||||
stats: BackendStats {
|
||||
commands_executed: 0,
|
||||
packages_queried: 0,
|
||||
transactions_committed: 0,
|
||||
cache_hit_rate: 1.0, // Mock always hits cache
|
||||
avg_command_time_ms: 0.1, // Very fast
|
||||
},
|
||||
packages,
|
||||
repositories: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a mock package.
|
||||
pub fn add_package(&mut self, package: Package) {
|
||||
self.packages.insert(package.name.clone(), package);
|
||||
}
|
||||
|
||||
/// Remove a mock package.
|
||||
pub fn remove_package(&mut self, name: &str) {
|
||||
self.packages.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AptBackend for MockBackend {
|
||||
fn name(&self) -> &'static str {
|
||||
"mock"
|
||||
}
|
||||
|
||||
fn version(&self) -> &'static str {
|
||||
"1.0.0-test"
|
||||
}
|
||||
|
||||
async fn is_available(&self) -> Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn resolve(&self, operations: &[Operation]) -> Result<Resolution> {
|
||||
let mut to_install = Vec::new();
|
||||
let mut to_remove = Vec::new();
|
||||
let mut to_upgrade = Vec::new();
|
||||
let mut conflicts = Vec::new();
|
||||
|
||||
for operation in operations {
|
||||
match operation {
|
||||
Operation::Install(pkg) => {
|
||||
if self.packages.contains_key(&pkg.name) {
|
||||
to_install.push(pkg.clone());
|
||||
} else {
|
||||
conflicts.push(format!("Package {} not found", pkg.name));
|
||||
}
|
||||
}
|
||||
Operation::Remove(pkg) => {
|
||||
if self.packages.contains_key(&pkg.name) {
|
||||
to_remove.push(pkg.clone());
|
||||
}
|
||||
}
|
||||
Operation::Upgrade(pkg) => {
|
||||
if self.packages.contains_key(&pkg.name) {
|
||||
to_upgrade.push(pkg.clone());
|
||||
} else {
|
||||
conflicts.push(format!("Package {} not found for upgrade", pkg.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Resolution {
|
||||
resolvable: conflicts.is_empty(),
|
||||
to_install,
|
||||
to_remove,
|
||||
to_upgrade,
|
||||
dependencies: Vec::new(),
|
||||
conflicts,
|
||||
summary: format!("Mock resolution: {} operations", operations.len()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn commit(&self, operations: &[Operation]) -> Result<()> {
|
||||
if self.config.enable_logging {
|
||||
eprintln!("Mock commit: {} operations", operations.len());
|
||||
for (i, op) in operations.iter().enumerate() {
|
||||
match op {
|
||||
Operation::Install(pkg) => eprintln!(" {}: Install {}", i + 1, pkg.name),
|
||||
Operation::Remove(pkg) => eprintln!(" {}: Remove {}", i + 1, pkg.name),
|
||||
Operation::Upgrade(pkg) => eprintln!(" {}: Upgrade {}", i + 1, pkg.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mock commit always succeeds
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn search_packages(&mut self, pattern: &str, _options: &QueryOptions) -> Result<Vec<Package>> {
|
||||
self.stats.packages_queried += 1;
|
||||
|
||||
let mut results = Vec::new();
|
||||
for (name, package) in &self.packages {
|
||||
if name.contains(pattern) {
|
||||
results.push(package.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn get_package_info(&mut self, name: &str, _options: &QueryOptions) -> Result<Option<Package>> {
|
||||
self.stats.packages_queried += 1;
|
||||
Ok(self.packages.get(name).cloned())
|
||||
}
|
||||
|
||||
async fn is_package_installed(&self, name: &str) -> Result<bool> {
|
||||
Ok(self.packages.contains_key(name))
|
||||
}
|
||||
|
||||
async fn get_installed_packages(&self, _options: &QueryOptions) -> Result<Vec<Package>> {
|
||||
Ok(self.packages.values().cloned().collect())
|
||||
}
|
||||
|
||||
async fn add_repository(&self, repo: &RepositoryInfo) -> Result<()> {
|
||||
if self.config.enable_logging {
|
||||
eprintln!("Mock add repository: {} at {}", repo.name, repo.url);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_repository(&self, name: &str) -> Result<()> {
|
||||
if self.config.enable_logging {
|
||||
eprintln!("Mock remove repository: {}", name);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_repositories(&self) -> Result<Vec<RepositoryInfo>> {
|
||||
Ok(self.repositories.values().cloned().collect())
|
||||
}
|
||||
|
||||
async fn update_cache(&self) -> Result<()> {
|
||||
if self.config.enable_logging {
|
||||
eprintln!("Mock update cache");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> Result<BackendStats> {
|
||||
Ok(self.stats.clone())
|
||||
}
|
||||
}
|
||||
455
apt-dnf-bridge-advanced/src/backend/shell_backend.rs
Normal file
455
apt-dnf-bridge-advanced/src/backend/shell_backend.rs
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
//! Shell backend implementation using apt commands.
|
||||
|
||||
use crate::backend::{AptBackend, BackendConfig, BackendStats, QueryOptions, RepositoryInfo, Resolution};
|
||||
use apt_dnf_bridge_core::{
|
||||
command::{AptCommands, validate_package_name, validate_repository_url},
|
||||
AptError, Operation, Package, Repository, Result,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Shell backend that uses apt commands via shell-out.
|
||||
pub struct ShellBackend {
|
||||
config: BackendConfig,
|
||||
stats: BackendStats,
|
||||
search_cache: HashMap<String, (Vec<Package>, Instant)>,
|
||||
info_cache: HashMap<String, (Option<Package>, Instant)>,
|
||||
}
|
||||
|
||||
impl ShellBackend {
|
||||
/// Create a new shell backend.
|
||||
pub fn new(config: BackendConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
stats: BackendStats {
|
||||
commands_executed: 0,
|
||||
packages_queried: 0,
|
||||
transactions_committed: 0,
|
||||
cache_hit_rate: 0.0,
|
||||
avg_command_time_ms: 0.0,
|
||||
},
|
||||
search_cache: HashMap::new(),
|
||||
info_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a command and update statistics.
|
||||
async fn execute_command<F, T>(&mut self, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce() -> Result<T>,
|
||||
{
|
||||
let start = Instant::now();
|
||||
let result = f()?;
|
||||
let duration = start.elapsed();
|
||||
|
||||
self.stats.commands_executed += 1;
|
||||
self.stats.avg_command_time_ms =
|
||||
(self.stats.avg_command_time_ms + duration.as_millis() as f64) / 2.0;
|
||||
|
||||
if self.config.enable_logging {
|
||||
eprintln!("Command executed in {:?}", duration);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Check if cache entry is still valid.
|
||||
fn is_cache_valid(&self, timestamp: Instant) -> bool {
|
||||
timestamp.elapsed() < Duration::from_secs(300) // 5 minutes
|
||||
}
|
||||
|
||||
/// Update cache hit rate.
|
||||
fn update_cache_hit_rate(&mut self) {
|
||||
let total_queries = self.stats.packages_queried;
|
||||
if total_queries > 0 {
|
||||
let cache_hits = self.search_cache.len() + self.info_cache.len();
|
||||
self.stats.cache_hit_rate = cache_hits as f64 / total_queries as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AptBackend for ShellBackend {
|
||||
fn name(&self) -> &'static str {
|
||||
"shell"
|
||||
}
|
||||
|
||||
fn version(&self) -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
|
||||
async fn is_available(&self) -> Result<bool> {
|
||||
// Check if apt is available
|
||||
let output = std::process::Command::new("apt")
|
||||
.arg("--version")
|
||||
.output();
|
||||
|
||||
Ok(output.is_ok() && output.unwrap().status.success())
|
||||
}
|
||||
|
||||
async fn resolve(&self, operations: &[Operation]) -> Result<Resolution> {
|
||||
if operations.is_empty() {
|
||||
return Ok(Resolution {
|
||||
resolvable: true,
|
||||
to_install: Vec::new(),
|
||||
to_remove: Vec::new(),
|
||||
to_upgrade: Vec::new(),
|
||||
dependencies: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
summary: "Empty transaction".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Separate operations by type
|
||||
let mut install_packages = Vec::new();
|
||||
let mut remove_packages = Vec::new();
|
||||
let mut upgrade_packages = Vec::new();
|
||||
|
||||
for operation in operations {
|
||||
match operation {
|
||||
Operation::Install(pkg) => install_packages.push(pkg.name.as_str()),
|
||||
Operation::Remove(pkg) => remove_packages.push(pkg.name.as_str()),
|
||||
Operation::Upgrade(pkg) => upgrade_packages.push(pkg.name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate install/upgrade operations
|
||||
if !install_packages.is_empty() || !upgrade_packages.is_empty() {
|
||||
let mut packages = install_packages;
|
||||
packages.extend(upgrade_packages);
|
||||
|
||||
let mut cmd = AptCommands::simulate_install(&packages);
|
||||
if self.config.enable_logging {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(Resolution {
|
||||
resolvable: false,
|
||||
to_install: Vec::new(),
|
||||
to_remove: Vec::new(),
|
||||
to_upgrade: Vec::new(),
|
||||
dependencies: Vec::new(),
|
||||
conflicts: vec![String::from_utf8_lossy(&output.stderr).to_string()],
|
||||
summary: "Transaction cannot be resolved".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// For remove operations, we can't easily simulate, so we just validate
|
||||
for package in &remove_packages {
|
||||
validate_package_name(package)?;
|
||||
}
|
||||
|
||||
Ok(Resolution {
|
||||
resolvable: true,
|
||||
to_install: operations.iter()
|
||||
.filter_map(|op| match op {
|
||||
Operation::Install(pkg) => Some(pkg.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
to_remove: operations.iter()
|
||||
.filter_map(|op| match op {
|
||||
Operation::Remove(pkg) => Some(pkg.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
to_upgrade: operations.iter()
|
||||
.filter_map(|op| match op {
|
||||
Operation::Upgrade(pkg) => Some(pkg.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
dependencies: Vec::new(), // Shell backend can't easily determine dependencies
|
||||
conflicts: Vec::new(),
|
||||
summary: format!("Transaction with {} operations", operations.len()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn commit(&self, operations: &[Operation]) -> Result<()> {
|
||||
if operations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Separate operations by type
|
||||
let mut install_packages = Vec::new();
|
||||
let mut remove_packages = Vec::new();
|
||||
let mut upgrade_packages = Vec::new();
|
||||
|
||||
for operation in operations {
|
||||
match operation {
|
||||
Operation::Install(pkg) => install_packages.push(pkg.name.as_str()),
|
||||
Operation::Remove(pkg) => remove_packages.push(pkg.name.as_str()),
|
||||
Operation::Upgrade(pkg) => upgrade_packages.push(pkg.name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute remove operations first
|
||||
if !remove_packages.is_empty() {
|
||||
let mut cmd = AptCommands::remove(&remove_packages);
|
||||
if self.config.enable_logging {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(crate::AptError::CommitFailed(
|
||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Execute install/upgrade operations
|
||||
if !install_packages.is_empty() || !upgrade_packages.is_empty() {
|
||||
let mut packages = install_packages;
|
||||
packages.extend(upgrade_packages);
|
||||
|
||||
let mut cmd = AptCommands::install(&packages);
|
||||
if self.config.enable_logging {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(crate::AptError::CommitFailed(
|
||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn search_packages(&mut self, pattern: &str, options: &QueryOptions) -> Result<Vec<Package>> {
|
||||
self.stats.packages_queried += 1;
|
||||
|
||||
// Check cache first
|
||||
if options.use_cache && self.config.use_cache {
|
||||
if let Some((cached, timestamp)) = self.search_cache.get(pattern) {
|
||||
if self.is_cache_valid(*timestamp) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validate_package_name(pattern)?;
|
||||
|
||||
let mut cmd = AptCommands::search(pattern);
|
||||
if options.enable_logging || self.config.enable_logging {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(crate::AptError::CommandExitCode {
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
output: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
let mut packages = Vec::new();
|
||||
|
||||
for line in output_str.lines() {
|
||||
if let Some((name, _description)) = line.split_once(" - ") {
|
||||
let package = Package::new(name.trim(), "", "unknown");
|
||||
packages.push(package);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply max_results limit
|
||||
if let Some(max) = options.max_results {
|
||||
packages.truncate(max);
|
||||
}
|
||||
|
||||
// Cache the results
|
||||
if options.use_cache && self.config.use_cache {
|
||||
self.search_cache.insert(pattern.to_string(), (packages.clone(), Instant::now()));
|
||||
}
|
||||
|
||||
self.update_cache_hit_rate();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
async fn get_package_info(&mut self, name: &str, options: &QueryOptions) -> Result<Option<Package>> {
|
||||
self.stats.packages_queried += 1;
|
||||
|
||||
// Check cache first
|
||||
if options.use_cache && self.config.use_cache {
|
||||
if let Some((cached, timestamp)) = self.info_cache.get(name) {
|
||||
if self.is_cache_valid(*timestamp) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validate_package_name(name)?;
|
||||
|
||||
let mut cmd = AptCommands::show(name);
|
||||
if options.enable_logging || self.config.enable_logging {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
let result = self.parse_package_info(&output_str)?;
|
||||
|
||||
// Cache the result
|
||||
if options.use_cache && self.config.use_cache {
|
||||
self.info_cache.insert(name.to_string(), (result.clone(), Instant::now()));
|
||||
}
|
||||
|
||||
self.update_cache_hit_rate();
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn is_package_installed(&self, name: &str) -> Result<bool> {
|
||||
validate_package_name(name)?;
|
||||
|
||||
let mut cmd = AptCommands::list_installed();
|
||||
if self.config.enable_logging {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
Ok(output.status.success())
|
||||
}
|
||||
|
||||
async fn get_installed_packages(&self, _options: &QueryOptions) -> Result<Vec<Package>> {
|
||||
let mut cmd = AptCommands::list_installed();
|
||||
if self.config.enable_logging {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(crate::AptError::CommandExitCode {
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
output: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
let mut packages = Vec::new();
|
||||
|
||||
for line in output_str.lines() {
|
||||
if line.starts_with("ii ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 3 {
|
||||
let name = parts[1];
|
||||
let version = parts[2];
|
||||
let architecture = parts.get(3).unwrap_or(&"unknown");
|
||||
|
||||
let package = Package::new(name, version, architecture);
|
||||
packages.push(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
async fn add_repository(&self, repo: &RepositoryInfo) -> Result<()> {
|
||||
validate_repository_url(&repo.url)?;
|
||||
|
||||
let mut repository = crate::Repository::new(&repo.name, &repo.url)?;
|
||||
for component in &repo.components {
|
||||
repository.add_component(component);
|
||||
}
|
||||
if !repo.enabled {
|
||||
repository.disable();
|
||||
}
|
||||
|
||||
repository.save_to_sources_list_d()?;
|
||||
repository.update_cache().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_repository(&self, _name: &str) -> Result<()> {
|
||||
// This is a simplified implementation
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"Repository removal not implemented in shell backend"
|
||||
)))
|
||||
}
|
||||
|
||||
async fn list_repositories(&self) -> Result<Vec<RepositoryInfo>> {
|
||||
// This is a simplified implementation
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn update_cache(&self) -> Result<()> {
|
||||
let output = AptCommands::update().output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(crate::AptError::CommandExitCode {
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
output: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> Result<BackendStats> {
|
||||
Ok(self.stats.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellBackend {
|
||||
/// Parse package information from apt-cache show output.
|
||||
fn parse_package_info(&self, output: &str) -> Result<Option<Package>> {
|
||||
let mut name = None;
|
||||
let mut version = None;
|
||||
let mut architecture = None;
|
||||
let mut description = None;
|
||||
let mut size = None;
|
||||
|
||||
// Check if we have any recognizable APT output format
|
||||
if !output.contains("Package:") && !output.contains("Version:") {
|
||||
return Err(crate::AptError::OutputFormatChanged(
|
||||
"No recognizable APT package information found".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
for line in output.lines() {
|
||||
if line.starts_with("Package: ") {
|
||||
name = Some(line[9..].trim().to_string());
|
||||
} else if line.starts_with("Version: ") {
|
||||
version = Some(line[9..].trim().to_string());
|
||||
} else if line.starts_with("Architecture: ") {
|
||||
architecture = Some(line[14..].trim().to_string());
|
||||
} else if line.starts_with("Description: ") {
|
||||
description = Some(line[13..].trim().to_string());
|
||||
} else if line.starts_with("Size: ") {
|
||||
if let Ok(s) = line[6..].trim().parse::<u64>() {
|
||||
size = Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (name.clone(), version.clone(), architecture.clone()) {
|
||||
(Some(name), Some(version), Some(architecture)) => Ok(Some(Package::new_full(
|
||||
&name, &version, &architecture, description, size,
|
||||
))),
|
||||
_ => {
|
||||
Err(crate::AptError::OutputFormatChanged(format!(
|
||||
"Incomplete package information parsed. Found: name={:?}, version={:?}, arch={:?}",
|
||||
name, version, architecture
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
apt-dnf-bridge-advanced/src/lib.rs
Normal file
12
apt-dnf-bridge-advanced/src/lib.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//! # APT-DNF Bridge Advanced
|
||||
//!
|
||||
//! Advanced features for apt-dnf-bridge including pluggable backends,
|
||||
//! caching, and enhanced error handling.
|
||||
|
||||
pub mod backend;
|
||||
pub mod package_v2;
|
||||
pub mod transaction_v2;
|
||||
|
||||
pub use backend::{AptBackend, BackendConfig, BackendFactory, BackendStats, QueryOptions, Resolution, RepositoryInfo};
|
||||
pub use package_v2::PackageDatabaseV2;
|
||||
pub use transaction_v2::TransactionV2;
|
||||
109
apt-dnf-bridge-advanced/src/package_v2.rs
Normal file
109
apt-dnf-bridge-advanced/src/package_v2.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
//! Enhanced package management using pluggable backends.
|
||||
|
||||
use crate::backend::{AptBackend, BackendConfig, BackendFactory, QueryOptions};
|
||||
use apt_dnf_bridge_core::{command, Package, Result};
|
||||
|
||||
/// Enhanced package database that uses pluggable backends.
|
||||
pub struct PackageDatabaseV2 {
|
||||
/// Backend for APT operations.
|
||||
backend: Box<dyn AptBackend>,
|
||||
/// Query options.
|
||||
query_options: QueryOptions,
|
||||
}
|
||||
|
||||
impl PackageDatabaseV2 {
|
||||
/// Create a new package database with auto-detected backend.
|
||||
pub async fn new() -> Result<Self> {
|
||||
let config = BackendConfig::default();
|
||||
let backend = BackendFactory::auto_detect(config).await?;
|
||||
|
||||
Ok(Self {
|
||||
backend,
|
||||
query_options: QueryOptions::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new package database with specific backend.
|
||||
pub async fn with_backend(backend: Box<dyn AptBackend>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
backend,
|
||||
query_options: QueryOptions::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new package database with shell backend.
|
||||
pub async fn with_shell_backend(config: BackendConfig) -> Result<Self> {
|
||||
let backend = BackendFactory::create_shell_backend(config)?;
|
||||
Ok(Self {
|
||||
backend,
|
||||
query_options: QueryOptions::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new package database with mock backend for testing.
|
||||
pub async fn with_mock_backend(config: BackendConfig) -> Result<Self> {
|
||||
let backend = BackendFactory::create_mock_backend(config)?;
|
||||
Ok(Self {
|
||||
backend,
|
||||
query_options: QueryOptions::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Enable command logging for debugging.
|
||||
pub fn enable_logging(&mut self) {
|
||||
self.query_options.enable_logging = true;
|
||||
}
|
||||
|
||||
/// Disable command logging.
|
||||
pub fn disable_logging(&mut self) {
|
||||
self.query_options.enable_logging = false;
|
||||
}
|
||||
|
||||
/// Enable caching.
|
||||
pub fn enable_caching(&mut self) {
|
||||
self.query_options.use_cache = true;
|
||||
}
|
||||
|
||||
/// Disable caching.
|
||||
pub fn disable_caching(&mut self) {
|
||||
self.query_options.use_cache = false;
|
||||
}
|
||||
|
||||
/// Set maximum number of results for queries.
|
||||
pub fn set_max_results(&mut self, max: Option<usize>) {
|
||||
self.query_options.max_results = max;
|
||||
}
|
||||
|
||||
/// Find packages by name pattern.
|
||||
pub async fn find_packages(&mut self, pattern: &str) -> Result<Vec<Package>> {
|
||||
crate::command::validate_package_name(pattern)?;
|
||||
self.backend.search_packages(pattern, &self.query_options).await
|
||||
}
|
||||
|
||||
/// Get detailed information about a specific package.
|
||||
pub async fn get_package_info(&mut self, name: &str) -> Result<Option<Package>> {
|
||||
crate::command::validate_package_name(name)?;
|
||||
self.backend.get_package_info(name, &self.query_options).await
|
||||
}
|
||||
|
||||
/// Check if a package is installed.
|
||||
pub async fn is_installed(&self, name: &str) -> Result<bool> {
|
||||
crate::command::validate_package_name(name)?;
|
||||
self.backend.is_package_installed(name).await
|
||||
}
|
||||
|
||||
/// Get installed packages.
|
||||
pub async fn get_installed_packages(&self) -> Result<Vec<Package>> {
|
||||
self.backend.get_installed_packages(&self.query_options).await
|
||||
}
|
||||
|
||||
/// Get the backend name and version.
|
||||
pub fn backend_info(&self) -> (String, String) {
|
||||
(self.backend.name().to_string(), self.backend.version().to_string())
|
||||
}
|
||||
|
||||
/// Get backend statistics.
|
||||
pub async fn get_backend_stats(&self) -> Result<crate::backend::BackendStats> {
|
||||
self.backend.get_stats().await
|
||||
}
|
||||
}
|
||||
179
apt-dnf-bridge-advanced/src/transaction_v2.rs
Normal file
179
apt-dnf-bridge-advanced/src/transaction_v2.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
//! Enhanced transaction management using pluggable backends.
|
||||
|
||||
use crate::backend::{AptBackend, BackendConfig, BackendFactory, Resolution};
|
||||
use apt_dnf_bridge_core::{command, AptError, Operation, Package, Result};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Enhanced transaction that uses pluggable backends.
|
||||
pub struct TransactionV2 {
|
||||
/// Backend for APT operations.
|
||||
backend: Box<dyn AptBackend>,
|
||||
/// List of operations in this transaction.
|
||||
operations: Vec<Operation>,
|
||||
/// Packages that were installed before this transaction (for rollback).
|
||||
previously_installed: HashSet<String>,
|
||||
/// Whether to enable logging.
|
||||
log_commands: bool,
|
||||
}
|
||||
|
||||
impl TransactionV2 {
|
||||
/// Create a new transaction with auto-detected backend.
|
||||
pub async fn new() -> Result<Self> {
|
||||
let config = BackendConfig::default();
|
||||
let backend = BackendFactory::auto_detect(config).await?;
|
||||
|
||||
Ok(Self {
|
||||
backend,
|
||||
operations: Vec::new(),
|
||||
previously_installed: HashSet::new(),
|
||||
log_commands: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new transaction with specific backend.
|
||||
pub async fn with_backend(backend: Box<dyn AptBackend>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
backend,
|
||||
operations: Vec::new(),
|
||||
previously_installed: HashSet::new(),
|
||||
log_commands: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new transaction with shell backend.
|
||||
pub async fn with_shell_backend(config: BackendConfig) -> Result<Self> {
|
||||
let backend = BackendFactory::create_shell_backend(config)?;
|
||||
Ok(Self {
|
||||
backend,
|
||||
operations: Vec::new(),
|
||||
previously_installed: HashSet::new(),
|
||||
log_commands: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new transaction with mock backend for testing.
|
||||
pub async fn with_mock_backend(config: BackendConfig) -> Result<Self> {
|
||||
let backend = BackendFactory::create_mock_backend(config)?;
|
||||
Ok(Self {
|
||||
backend,
|
||||
operations: Vec::new(),
|
||||
previously_installed: HashSet::new(),
|
||||
log_commands: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Enable command logging for debugging.
|
||||
pub fn enable_logging(&mut self) {
|
||||
self.log_commands = true;
|
||||
}
|
||||
|
||||
/// Disable command logging.
|
||||
pub fn disable_logging(&mut self) {
|
||||
self.log_commands = false;
|
||||
}
|
||||
|
||||
/// Add an install operation to the transaction.
|
||||
pub async fn add_install(&mut self, package: Package) -> Result<()> {
|
||||
crate::command::validate_package_name(&package.name)?;
|
||||
self.operations.push(Operation::Install(package));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a remove operation to the transaction.
|
||||
pub async fn add_remove(&mut self, package: Package) -> Result<()> {
|
||||
crate::command::validate_package_name(&package.name)?;
|
||||
self.operations.push(Operation::Remove(package));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add an upgrade operation to the transaction.
|
||||
pub async fn add_upgrade(&mut self, package: Package) -> Result<()> {
|
||||
crate::command::validate_package_name(&package.name)?;
|
||||
self.operations.push(Operation::Upgrade(package));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all operations in this transaction.
|
||||
pub fn operations(&self) -> &[Operation] {
|
||||
&self.operations
|
||||
}
|
||||
|
||||
/// Check if the transaction is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.operations.is_empty()
|
||||
}
|
||||
|
||||
/// Get the number of operations in this transaction.
|
||||
pub fn len(&self) -> usize {
|
||||
self.operations.len()
|
||||
}
|
||||
|
||||
/// Resolve the transaction using the backend.
|
||||
pub async fn resolve(&self) -> Result<Resolution> {
|
||||
if self.operations.is_empty() {
|
||||
return Ok(Resolution {
|
||||
resolvable: true,
|
||||
to_install: Vec::new(),
|
||||
to_remove: Vec::new(),
|
||||
to_upgrade: Vec::new(),
|
||||
dependencies: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
summary: "Empty transaction".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
self.backend.resolve(&self.operations).await
|
||||
}
|
||||
|
||||
/// Commit the transaction using the backend.
|
||||
pub async fn commit(&mut self) -> Result<()> {
|
||||
if self.operations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Capture current state for potential rollback
|
||||
self.capture_state().await?;
|
||||
|
||||
// Use backend to commit
|
||||
self.backend.commit(&self.operations).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Capture the current system state for potential rollback.
|
||||
async fn capture_state(&mut self) -> Result<()> {
|
||||
// This is a simplified implementation
|
||||
// In a real implementation, you'd capture the current package state
|
||||
self.previously_installed.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to rollback the transaction (best-effort).
|
||||
pub async fn rollback(&self) -> Result<()> {
|
||||
if self.previously_installed.is_empty() {
|
||||
return Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"No previous state captured for rollback"
|
||||
)));
|
||||
}
|
||||
|
||||
// This is a simplified rollback implementation
|
||||
Err(crate::AptError::Generic(anyhow::anyhow!(
|
||||
"Rollback not fully implemented - manual intervention may be required"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Clear all operations from the transaction.
|
||||
pub fn clear(&mut self) {
|
||||
self.operations.clear();
|
||||
}
|
||||
|
||||
/// Get the backend name and version.
|
||||
pub fn backend_info(&self) -> (String, String) {
|
||||
(self.backend.name().to_string(), self.backend.version().to_string())
|
||||
}
|
||||
|
||||
/// Get backend statistics.
|
||||
pub async fn get_backend_stats(&self) -> Result<crate::backend::BackendStats> {
|
||||
self.backend.get_stats().await
|
||||
}
|
||||
}
|
||||
15
apt-dnf-bridge-core/Cargo.toml
Normal file
15
apt-dnf-bridge-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "apt-dnf-bridge-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Minimal shell-based APT transaction API (DNF-like)"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
175
apt-dnf-bridge-core/src/command.rs
Normal file
175
apt-dnf-bridge-core/src/command.rs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
//! Secure command execution for APT operations.
|
||||
|
||||
use crate::{AptError, Result};
|
||||
use std::process::Command;
|
||||
|
||||
/// Secure command execution with argument sanitization and logging.
|
||||
pub struct SecureCommand {
|
||||
program: String,
|
||||
args: Vec<String>,
|
||||
log_commands: bool,
|
||||
}
|
||||
|
||||
impl SecureCommand {
|
||||
/// Create a new secure command executor.
|
||||
pub fn new(program: &str) -> Self {
|
||||
Self {
|
||||
program: program.to_string(),
|
||||
args: Vec::new(),
|
||||
log_commands: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable command logging for debugging.
|
||||
pub fn with_logging(mut self) -> Self {
|
||||
self.log_commands = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a sanitized argument.
|
||||
pub fn arg(mut self, arg: &str) -> Self {
|
||||
let sanitized = self.sanitize_argument(arg);
|
||||
self.args.push(sanitized);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple sanitized arguments.
|
||||
pub fn args(mut self, args: &[&str]) -> Self {
|
||||
for arg in args {
|
||||
let sanitized = self.sanitize_argument(arg);
|
||||
self.args.push(sanitized);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the command and return the output.
|
||||
pub fn output(self) -> Result<std::process::Output> {
|
||||
if self.log_commands {
|
||||
eprintln!("Executing: {} {}", self.program, self.args.join(" "));
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(&self.program);
|
||||
cmd.args(&self.args);
|
||||
|
||||
let output = cmd.output().map_err(|_e| AptError::CommandFailed {
|
||||
command: format!("{} {}", self.program, self.args.join(" ")),
|
||||
})?;
|
||||
|
||||
if self.log_commands {
|
||||
eprintln!("Exit code: {}", output.status.code().unwrap_or(-1));
|
||||
if !output.stderr.is_empty() {
|
||||
eprintln!("Stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Sanitize command arguments to prevent injection attacks.
|
||||
fn sanitize_argument(&self, arg: &str) -> String {
|
||||
// Remove potentially dangerous characters
|
||||
arg.chars()
|
||||
.filter(|c| !matches!(c, ';' | '|' | '&' | '$' | '`' | '"' | '\'' | '\\' | '\n' | '\r'))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Predefined APT command configurations with stable flags.
|
||||
pub struct AptCommands;
|
||||
|
||||
impl AptCommands {
|
||||
/// Create a secure apt-cache search command.
|
||||
pub fn search(pattern: &str) -> SecureCommand {
|
||||
SecureCommand::new("apt-cache")
|
||||
.args(&["search", "--names-only", pattern])
|
||||
}
|
||||
|
||||
/// Create a secure apt-cache show command.
|
||||
pub fn show(package: &str) -> SecureCommand {
|
||||
SecureCommand::new("apt-cache")
|
||||
.args(&["show", package])
|
||||
}
|
||||
|
||||
/// Create a secure apt install command with stable flags.
|
||||
pub fn install(packages: &[&str]) -> SecureCommand {
|
||||
let mut cmd = SecureCommand::new("apt")
|
||||
.args(&["install", "--assume-yes", "--no-install-recommends", "--quiet"]);
|
||||
|
||||
for package in packages {
|
||||
cmd = cmd.arg(package);
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Create a secure apt remove command with stable flags.
|
||||
pub fn remove(packages: &[&str]) -> SecureCommand {
|
||||
let mut cmd = SecureCommand::new("apt")
|
||||
.args(&["remove", "--assume-yes", "--quiet"]);
|
||||
|
||||
for package in packages {
|
||||
cmd = cmd.arg(package);
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Create a secure apt update command.
|
||||
pub fn update() -> SecureCommand {
|
||||
SecureCommand::new("apt")
|
||||
.args(&["update", "--quiet"])
|
||||
}
|
||||
|
||||
/// Create a secure apt list command for installed packages.
|
||||
pub fn list_installed() -> SecureCommand {
|
||||
SecureCommand::new("dpkg")
|
||||
.args(&["-l"])
|
||||
}
|
||||
|
||||
/// Create a secure apt simulate command for dry-run operations.
|
||||
pub fn simulate_install(packages: &[&str]) -> SecureCommand {
|
||||
let mut cmd = SecureCommand::new("apt")
|
||||
.args(&["install", "--simulate", "--assume-yes", "--no-install-recommends", "--quiet"]);
|
||||
|
||||
for package in packages {
|
||||
cmd = cmd.arg(package);
|
||||
}
|
||||
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate package names to prevent injection.
|
||||
pub fn validate_package_name(name: &str) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
return Err(AptError::InvalidPackageSpec("Package name cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
if name.len() > 100 {
|
||||
return Err(AptError::InvalidPackageSpec("Package name too long".to_string()));
|
||||
}
|
||||
|
||||
// Check for dangerous characters
|
||||
if name.chars().any(|c| matches!(c, ';' | '|' | '&' | '$' | '`' | '"' | '\'' | '\\' | '\n' | '\r' | ' ')) {
|
||||
return Err(AptError::InvalidPackageSpec(
|
||||
"Package name contains invalid characters".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validate repository URLs to prevent injection.
|
||||
pub fn validate_repository_url(url: &str) -> Result<()> {
|
||||
if url.is_empty() {
|
||||
return Err(AptError::Generic(anyhow::anyhow!("Repository URL cannot be empty")));
|
||||
}
|
||||
|
||||
if !url.starts_with("http://") && !url.starts_with("https://") && !url.starts_with("file://") {
|
||||
return Err(AptError::Generic(anyhow::anyhow!(
|
||||
"Repository URL must start with http://, https://, or file://"
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
58
apt-dnf-bridge-core/src/error.rs
Normal file
58
apt-dnf-bridge-core/src/error.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
//! Error types for the APT wrapper crate.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Result type alias for the APT wrapper crate.
|
||||
pub type Result<T> = std::result::Result<T, AptError>;
|
||||
|
||||
/// Errors that can occur when using the APT wrapper.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AptError {
|
||||
/// APT command execution failed.
|
||||
#[error("APT command failed: {command}")]
|
||||
CommandFailed { command: String },
|
||||
|
||||
/// APT command returned non-zero exit code.
|
||||
#[error("APT command failed with exit code {code}: {output}")]
|
||||
CommandExitCode { code: i32, output: String },
|
||||
|
||||
/// Failed to parse APT output.
|
||||
#[error("Failed to parse APT output: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
/// APT output format changed unexpectedly.
|
||||
#[error("APT output format changed unexpectedly. This may indicate a new APT version. Raw output: {0}")]
|
||||
OutputFormatChanged(String),
|
||||
|
||||
/// Package not found.
|
||||
#[error("Package not found: {0}")]
|
||||
PackageNotFound(String),
|
||||
|
||||
/// Repository not found.
|
||||
#[error("Repository not found: {0}")]
|
||||
RepositoryNotFound(String),
|
||||
|
||||
/// Invalid package specification.
|
||||
#[error("Invalid package specification: {0}")]
|
||||
InvalidPackageSpec(String),
|
||||
|
||||
/// Transaction resolution failed.
|
||||
#[error("Transaction resolution failed: {0}")]
|
||||
ResolutionFailed(String),
|
||||
|
||||
/// Transaction commit failed.
|
||||
#[error("Transaction commit failed: {0}")]
|
||||
CommitFailed(String),
|
||||
|
||||
/// IO error.
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// UTF-8 conversion error.
|
||||
#[error("UTF-8 conversion error: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
|
||||
/// Generic error.
|
||||
#[error("Generic error: {0}")]
|
||||
Generic(#[from] anyhow::Error),
|
||||
}
|
||||
15
apt-dnf-bridge-core/src/lib.rs
Normal file
15
apt-dnf-bridge-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//! # APT-DNF Bridge Core
|
||||
//!
|
||||
//! Minimal shell-based APT transaction API that provides DNF-like semantics.
|
||||
//! This is the core implementation that uses shell-out to APT commands.
|
||||
|
||||
pub mod command;
|
||||
pub mod error;
|
||||
pub mod package;
|
||||
pub mod repository;
|
||||
pub mod transaction;
|
||||
|
||||
pub use error::{AptError, Result};
|
||||
pub use package::{Package, PackageDatabase};
|
||||
pub use repository::{Repository, RepositoryManager};
|
||||
pub use transaction::{Operation, Transaction};
|
||||
273
apt-dnf-bridge-core/src/package.rs
Normal file
273
apt-dnf-bridge-core/src/package.rs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
//! Package management for the APT wrapper.
|
||||
|
||||
use crate::{AptError, Result, command::{AptCommands, validate_package_name}};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Represents a package in the APT system.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Package {
|
||||
/// Package name.
|
||||
pub name: String,
|
||||
/// Package version.
|
||||
pub version: String,
|
||||
/// Package architecture.
|
||||
pub architecture: String,
|
||||
/// Package description.
|
||||
pub description: Option<String>,
|
||||
/// Package size in bytes.
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
/// Create a new package with the given name, version, and architecture.
|
||||
pub fn new(name: &str, version: &str, architecture: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
version: version.to_string(),
|
||||
architecture: architecture.to_string(),
|
||||
description: None,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new package with all fields.
|
||||
pub fn new_full(
|
||||
name: &str,
|
||||
version: &str,
|
||||
architecture: &str,
|
||||
description: Option<String>,
|
||||
size: Option<u64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
version: version.to_string(),
|
||||
architecture: architecture.to_string(),
|
||||
description,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full package specification (name=version:architecture).
|
||||
pub fn spec(&self) -> String {
|
||||
format!("{}={}:{}", self.name, self.version, self.architecture)
|
||||
}
|
||||
|
||||
/// Get the package name with version.
|
||||
pub fn name_version(&self) -> String {
|
||||
format!("{}={}", self.name, self.version)
|
||||
}
|
||||
}
|
||||
|
||||
/// Package database for querying packages with caching.
|
||||
pub struct PackageDatabase {
|
||||
/// Cache for package search results.
|
||||
search_cache: HashMap<String, Vec<Package>>,
|
||||
/// Cache for package info results.
|
||||
info_cache: HashMap<String, Option<Package>>,
|
||||
/// Whether to enable logging.
|
||||
log_commands: bool,
|
||||
}
|
||||
|
||||
impl PackageDatabase {
|
||||
/// Create a new package database.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
search_cache: HashMap::new(),
|
||||
info_cache: HashMap::new(),
|
||||
log_commands: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new package database with logging enabled.
|
||||
pub fn new_with_logging() -> Self {
|
||||
Self {
|
||||
search_cache: HashMap::new(),
|
||||
info_cache: HashMap::new(),
|
||||
log_commands: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable command logging for debugging.
|
||||
pub fn enable_logging(&mut self) {
|
||||
self.log_commands = true;
|
||||
}
|
||||
|
||||
/// Disable command logging.
|
||||
pub fn disable_logging(&mut self) {
|
||||
self.log_commands = false;
|
||||
}
|
||||
|
||||
/// Clear all caches.
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.search_cache.clear();
|
||||
self.info_cache.clear();
|
||||
}
|
||||
|
||||
/// Find packages by name pattern with caching.
|
||||
pub async fn find_packages(&mut self, pattern: &str) -> Result<Vec<Package>> {
|
||||
// Check cache first
|
||||
if let Some(cached) = self.search_cache.get(pattern) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
|
||||
validate_package_name(pattern)?;
|
||||
|
||||
let mut cmd = AptCommands::search(pattern);
|
||||
if self.log_commands {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(AptError::CommandExitCode {
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
output: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
let mut packages = Vec::new();
|
||||
|
||||
for line in output_str.lines() {
|
||||
if let Some((name, _description)) = line.split_once(" - ") {
|
||||
let package = Package::new(name.trim(), "", "unknown");
|
||||
packages.push(package);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the results
|
||||
self.search_cache.insert(pattern.to_string(), packages.clone());
|
||||
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
/// Get detailed information about a specific package with caching.
|
||||
pub async fn get_package_info(&mut self, name: &str) -> Result<Option<Package>> {
|
||||
// Check cache first
|
||||
if let Some(cached) = self.info_cache.get(name) {
|
||||
return Ok(cached.clone());
|
||||
}
|
||||
|
||||
validate_package_name(name)?;
|
||||
|
||||
let mut cmd = AptCommands::show(name);
|
||||
if self.log_commands {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
let result = self.parse_package_info(&output_str)?;
|
||||
|
||||
// Cache the result
|
||||
self.info_cache.insert(name.to_string(), result.clone());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Parse package information from apt-cache show output.
|
||||
///
|
||||
/// This method is designed to be resilient to minor APT output format changes.
|
||||
/// If the output format changes significantly, it will return a descriptive error
|
||||
/// rather than silently failing.
|
||||
fn parse_package_info(&self, output: &str) -> Result<Option<Package>> {
|
||||
let mut name = None;
|
||||
let mut version = None;
|
||||
let mut architecture = None;
|
||||
let mut description = None;
|
||||
let mut size = None;
|
||||
|
||||
// Check if we have any recognizable APT output format
|
||||
if !output.contains("Package:") && !output.contains("Version:") {
|
||||
return Err(AptError::OutputFormatChanged(
|
||||
"No recognizable APT package information found".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
for line in output.lines() {
|
||||
if line.starts_with("Package: ") {
|
||||
name = Some(line[9..].trim().to_string());
|
||||
} else if line.starts_with("Version: ") {
|
||||
version = Some(line[9..].trim().to_string());
|
||||
} else if line.starts_with("Architecture: ") {
|
||||
architecture = Some(line[14..].trim().to_string());
|
||||
} else if line.starts_with("Description: ") {
|
||||
description = Some(line[13..].trim().to_string());
|
||||
} else if line.starts_with("Size: ") {
|
||||
if let Ok(s) = line[6..].trim().parse::<u64>() {
|
||||
size = Some(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (name.clone(), version.clone(), architecture.clone()) {
|
||||
(Some(name), Some(version), Some(architecture)) => Ok(Some(Package::new_full(
|
||||
&name, &version, &architecture, description, size,
|
||||
))),
|
||||
_ => {
|
||||
// If we found some APT output but couldn't parse it completely,
|
||||
// it might be a format change
|
||||
Err(AptError::OutputFormatChanged(format!(
|
||||
"Incomplete package information parsed. Found: name={:?}, version={:?}, arch={:?}",
|
||||
name, version, architecture
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a package is installed.
|
||||
pub async fn is_installed(&self, name: &str) -> Result<bool> {
|
||||
validate_package_name(name)?;
|
||||
|
||||
let mut cmd = AptCommands::list_installed();
|
||||
if self.log_commands {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
Ok(output.status.success())
|
||||
}
|
||||
|
||||
/// Get installed packages.
|
||||
pub async fn get_installed_packages(&self) -> Result<Vec<Package>> {
|
||||
let mut cmd = AptCommands::list_installed();
|
||||
if self.log_commands {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(AptError::CommandExitCode {
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
output: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let output_str = String::from_utf8(output.stdout)?;
|
||||
let mut packages = Vec::new();
|
||||
|
||||
for line in output_str.lines() {
|
||||
if line.starts_with("ii ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 3 {
|
||||
let name = parts[1];
|
||||
let version = parts[2];
|
||||
let architecture = parts.get(3).unwrap_or(&"unknown");
|
||||
|
||||
let package = Package::new(name, version, architecture);
|
||||
packages.push(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(packages)
|
||||
}
|
||||
}
|
||||
159
apt-dnf-bridge-core/src/repository.rs
Normal file
159
apt-dnf-bridge-core/src/repository.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
//! Repository management for the APT wrapper.
|
||||
|
||||
use crate::{AptError, Result, command::{AptCommands, validate_repository_url}};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Represents an APT repository.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Repository {
|
||||
/// Repository name.
|
||||
pub name: String,
|
||||
/// Repository URL.
|
||||
pub url: String,
|
||||
/// Repository components (e.g., main, contrib, non-free).
|
||||
pub components: Vec<String>,
|
||||
/// Whether the repository is enabled.
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
/// Create a new repository with the given name and URL.
|
||||
pub fn new(name: &str, url: &str) -> Result<Self> {
|
||||
validate_repository_url(url)?;
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
url: url.to_string(),
|
||||
components: Vec::new(),
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a component to the repository.
|
||||
pub fn add_component(&mut self, component: &str) {
|
||||
if !self.components.contains(&component.to_string()) {
|
||||
self.components.push(component.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a component from the repository.
|
||||
pub fn remove_component(&mut self, component: &str) {
|
||||
self.components.retain(|c| c != component);
|
||||
}
|
||||
|
||||
/// Enable the repository.
|
||||
pub fn enable(&mut self) {
|
||||
self.enabled = true;
|
||||
}
|
||||
|
||||
/// Disable the repository.
|
||||
pub fn disable(&mut self) {
|
||||
self.enabled = false;
|
||||
}
|
||||
|
||||
/// Get the sources.list entry for this repository.
|
||||
pub fn to_sources_list_entry(&self) -> String {
|
||||
if !self.enabled {
|
||||
return format!("# {}", self.to_sources_list_entry_enabled());
|
||||
}
|
||||
|
||||
if self.components.is_empty() {
|
||||
format!("deb {} ./\n", self.url)
|
||||
} else {
|
||||
format!("deb {} ./\n", self.url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the sources.list entry for this repository (enabled version).
|
||||
fn to_sources_list_entry_enabled(&self) -> String {
|
||||
if self.components.is_empty() {
|
||||
format!("deb {} ./\n", self.url)
|
||||
} else {
|
||||
format!("deb {} ./\n", self.url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Save the repository to sources.list.d/ directory (recommended).
|
||||
pub fn save_to_sources_list_d(&self) -> Result<()> {
|
||||
let filename = format!("{}.list", self.name);
|
||||
let path = PathBuf::from("/etc/apt/sources.list.d").join(filename);
|
||||
self.save_to_sources_list_path(&path)
|
||||
}
|
||||
|
||||
/// Save the repository to sources.list (not recommended for production).
|
||||
pub fn save_to_sources_list(&self) -> Result<()> {
|
||||
self.save_to_sources_list_path("/etc/apt/sources.list")
|
||||
}
|
||||
|
||||
/// Save the repository to a specific sources.list file.
|
||||
pub fn save_to_sources_list_path<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||
let entry = self.to_sources_list_entry();
|
||||
|
||||
// Ensure the directory exists
|
||||
if let Some(parent) = path.as_ref().parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| AptError::Io(e))?;
|
||||
}
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.map_err(|e| AptError::Io(e))?;
|
||||
|
||||
file.write_all(entry.as_bytes())
|
||||
.map_err(|e| AptError::Io(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the package cache for this repository.
|
||||
pub async fn update_cache(&self) -> Result<()> {
|
||||
let output = AptCommands::update().output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(AptError::CommandExitCode {
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
output: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Repository manager for handling multiple repositories.
|
||||
pub struct RepositoryManager;
|
||||
|
||||
impl RepositoryManager {
|
||||
/// Create a new repository manager.
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Add a repository to the system (recommended: uses sources.list.d/).
|
||||
pub async fn add_repository(&self, repository: &Repository) -> Result<()> {
|
||||
repository.save_to_sources_list_d()?;
|
||||
repository.update_cache().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a repository from the system.
|
||||
pub async fn remove_repository(&self, _name: &str) -> Result<()> {
|
||||
// This is a simplified implementation
|
||||
// In a real implementation, you'd need to parse sources.list and remove the entry
|
||||
Err(AptError::Generic(anyhow::anyhow!(
|
||||
"Repository removal not implemented yet"
|
||||
)))
|
||||
}
|
||||
|
||||
/// List all repositories.
|
||||
pub async fn list_repositories(&self) -> Result<Vec<Repository>> {
|
||||
// This is a simplified implementation
|
||||
// In a real implementation, you'd parse sources.list
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
250
apt-dnf-bridge-core/src/transaction.rs
Normal file
250
apt-dnf-bridge-core/src/transaction.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
//! Transaction management for the APT wrapper.
|
||||
|
||||
use crate::{AptError, Package, Result, command::{AptCommands, validate_package_name}};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Represents an operation in a transaction.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Operation {
|
||||
/// Install a package.
|
||||
Install(Package),
|
||||
/// Remove a package.
|
||||
Remove(Package),
|
||||
/// Upgrade a package.
|
||||
Upgrade(Package),
|
||||
}
|
||||
|
||||
/// A transaction that can be built, resolved, and committed.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Transaction {
|
||||
/// List of operations in this transaction.
|
||||
operations: Vec<Operation>,
|
||||
/// Packages that were installed before this transaction (for rollback).
|
||||
previously_installed: HashSet<String>,
|
||||
/// Whether to enable logging.
|
||||
log_commands: bool,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
/// Create a new empty transaction.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
operations: Vec::new(),
|
||||
previously_installed: HashSet::new(),
|
||||
log_commands: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new transaction with logging enabled.
|
||||
pub fn new_with_logging() -> Self {
|
||||
Self {
|
||||
operations: Vec::new(),
|
||||
previously_installed: HashSet::new(),
|
||||
log_commands: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable command logging for debugging.
|
||||
pub fn enable_logging(&mut self) {
|
||||
self.log_commands = true;
|
||||
}
|
||||
|
||||
/// Disable command logging.
|
||||
pub fn disable_logging(&mut self) {
|
||||
self.log_commands = false;
|
||||
}
|
||||
|
||||
/// Add an install operation to the transaction.
|
||||
pub async fn add_install(&mut self, package: Package) -> Result<()> {
|
||||
validate_package_name(&package.name)?;
|
||||
self.operations.push(Operation::Install(package));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a remove operation to the transaction.
|
||||
pub async fn add_remove(&mut self, package: Package) -> Result<()> {
|
||||
validate_package_name(&package.name)?;
|
||||
self.operations.push(Operation::Remove(package));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add an upgrade operation to the transaction.
|
||||
pub async fn add_upgrade(&mut self, package: Package) -> Result<()> {
|
||||
validate_package_name(&package.name)?;
|
||||
self.operations.push(Operation::Upgrade(package));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all operations in this transaction.
|
||||
pub fn operations(&self) -> &[Operation] {
|
||||
&self.operations
|
||||
}
|
||||
|
||||
/// Check if the transaction is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.operations.is_empty()
|
||||
}
|
||||
|
||||
/// Get the number of operations in this transaction.
|
||||
pub fn len(&self) -> usize {
|
||||
self.operations.len()
|
||||
}
|
||||
|
||||
/// Resolve the transaction by simulating the operations.
|
||||
///
|
||||
/// **IMPORTANT**: This is an approximation of DNF's resolve stage. APT's simulation
|
||||
/// output format is not guaranteed stable, and this method may not catch all edge cases
|
||||
/// that DNF's resolve would catch. Callers should expect some differences from DNF behavior.
|
||||
///
|
||||
/// This method:
|
||||
/// 1. Validates that all packages exist
|
||||
/// 2. Simulates the transaction to check for conflicts
|
||||
/// 3. Provides basic dependency resolution validation
|
||||
pub async fn resolve(&self) -> Result<()> {
|
||||
if self.operations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Separate operations by type
|
||||
let mut install_packages = Vec::new();
|
||||
let mut remove_packages = Vec::new();
|
||||
let mut upgrade_packages = Vec::new();
|
||||
|
||||
for operation in &self.operations {
|
||||
match operation {
|
||||
Operation::Install(pkg) => install_packages.push(pkg.name.as_str()),
|
||||
Operation::Remove(pkg) => remove_packages.push(pkg.name.as_str()),
|
||||
Operation::Upgrade(pkg) => upgrade_packages.push(pkg.name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate install/upgrade operations
|
||||
if !install_packages.is_empty() || !upgrade_packages.is_empty() {
|
||||
let mut packages = install_packages;
|
||||
packages.extend(upgrade_packages);
|
||||
|
||||
let mut cmd = AptCommands::simulate_install(&packages);
|
||||
if self.log_commands {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(AptError::ResolutionFailed(
|
||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// For remove operations, we can't easily simulate, so we just validate package names
|
||||
for package in &remove_packages {
|
||||
validate_package_name(package)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commit the transaction by executing the operations.
|
||||
///
|
||||
/// **IMPORTANT**: This method does NOT provide true atomicity at the system level.
|
||||
/// APT operations are not inherently atomic like OSTree transactions. For true
|
||||
/// atomicity, the calling code (e.g., apt-ostree) must handle this at a higher level,
|
||||
/// such as by operating on a temporary directory and performing an atomic pivot_root.
|
||||
///
|
||||
/// This method provides transaction-like semantics by:
|
||||
/// 1. Validating all operations can be performed (via resolve())
|
||||
/// 2. Executing operations in a logical order (remove, then install/upgrade)
|
||||
/// 3. Failing fast if any operation fails
|
||||
pub async fn commit(&mut self) -> Result<()> {
|
||||
if self.operations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Capture current state for potential rollback
|
||||
self.capture_state().await?;
|
||||
|
||||
// Separate operations by type
|
||||
let mut install_packages = Vec::new();
|
||||
let mut remove_packages = Vec::new();
|
||||
let mut upgrade_packages = Vec::new();
|
||||
|
||||
for operation in &self.operations {
|
||||
match operation {
|
||||
Operation::Install(pkg) => install_packages.push(pkg.name.as_str()),
|
||||
Operation::Remove(pkg) => remove_packages.push(pkg.name.as_str()),
|
||||
Operation::Upgrade(pkg) => upgrade_packages.push(pkg.name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute remove operations first
|
||||
if !remove_packages.is_empty() {
|
||||
let mut cmd = AptCommands::remove(&remove_packages);
|
||||
if self.log_commands {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(AptError::CommitFailed(
|
||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Execute install/upgrade operations
|
||||
if !install_packages.is_empty() || !upgrade_packages.is_empty() {
|
||||
let mut packages = install_packages;
|
||||
packages.extend(upgrade_packages);
|
||||
|
||||
let mut cmd = AptCommands::install(&packages);
|
||||
if self.log_commands {
|
||||
cmd = cmd.with_logging();
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(AptError::CommitFailed(
|
||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Capture the current system state for potential rollback.
|
||||
async fn capture_state(&mut self) -> Result<()> {
|
||||
// This is a simplified implementation
|
||||
// In a real implementation, you'd capture the current package state
|
||||
// For now, we'll just clear the previous state
|
||||
self.previously_installed.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to rollback the transaction (best-effort).
|
||||
///
|
||||
/// This method attempts to restore the system to its previous state, but
|
||||
/// cannot guarantee complete rollback due to APT's limitations.
|
||||
pub async fn rollback(&self) -> Result<()> {
|
||||
if self.previously_installed.is_empty() {
|
||||
return Err(AptError::Generic(anyhow::anyhow!(
|
||||
"No previous state captured for rollback"
|
||||
)));
|
||||
}
|
||||
|
||||
// This is a simplified rollback implementation
|
||||
// In a real implementation, you'd restore the previous package state
|
||||
Err(AptError::Generic(anyhow::anyhow!(
|
||||
"Rollback not fully implemented - manual intervention may be required"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Clear all operations from the transaction.
|
||||
pub fn clear(&mut self) {
|
||||
self.operations.clear();
|
||||
}
|
||||
}
|
||||
37
apt-dnf-bridge/Cargo.toml
Normal file
37
apt-dnf-bridge/Cargo.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "apt-dnf-bridge"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "DNF-like API for APT, with optional advanced features"
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[dependencies]
|
||||
apt-dnf-bridge-core = { path = "../apt-dnf-bridge-core" }
|
||||
apt-dnf-bridge-advanced = { path = "../apt-dnf-bridge-advanced", optional = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
[features]
|
||||
default = [] # Core-only by default
|
||||
advanced = ["apt-dnf-bridge-advanced"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "basic_usage"
|
||||
path = "../examples/basic_usage.rs"
|
||||
|
||||
[[example]]
|
||||
name = "package_query"
|
||||
path = "../examples/package_query.rs"
|
||||
|
||||
[[example]]
|
||||
name = "atomicity_notes"
|
||||
path = "../examples/atomicity_notes.rs"
|
||||
|
||||
[[example]]
|
||||
name = "backend_selection"
|
||||
path = "../examples/backend_selection.rs"
|
||||
47
apt-dnf-bridge/src/lib.rs
Normal file
47
apt-dnf-bridge/src/lib.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
//! # APT-DNF Bridge
|
||||
//!
|
||||
//! A DNF-like bridge around APT for apt-ostree integration.
|
||||
//! This crate provides a transaction-based API that makes APT work like DNF.
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! Core (minimal, shell-out only):
|
||||
//! ```toml
|
||||
//! apt-dnf-bridge = "0.1"
|
||||
//! ```
|
||||
//!
|
||||
//! With advanced features (pluggable backends, caching):
|
||||
//! ```toml
|
||||
//! apt-dnf-bridge = { version = "0.1", features = ["advanced"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use apt_dnf_bridge::{Transaction, Package, Repository};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! // Create a transaction
|
||||
//! let mut tx = Transaction::new();
|
||||
//!
|
||||
//! // Add packages to install
|
||||
//! let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64");
|
||||
//! tx.add_install(vim).await?;
|
||||
//!
|
||||
//! // Resolve dependencies (APT handles automatically)
|
||||
//! tx.resolve().await?;
|
||||
//!
|
||||
//! // Commit the transaction
|
||||
//! tx.commit().await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
// Always expose the core API
|
||||
pub use apt_dnf_bridge_core::*;
|
||||
|
||||
// Conditionally expose advanced features
|
||||
#[cfg(feature = "advanced")]
|
||||
pub use apt_dnf_bridge_advanced::*;
|
||||
63
examples/atomicity_notes.rs
Normal file
63
examples/atomicity_notes.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
//! Example demonstrating the atomicity limitations of the APT-DNF Bridge.
|
||||
//!
|
||||
//! This example shows how the APT-DNF Bridge provides transaction-like semantics
|
||||
//! but does NOT provide true system-level atomicity.
|
||||
|
||||
use apt_dnf_bridge::{Package, Transaction};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("APT-DNF Bridge - Atomicity Notes Example");
|
||||
println!("=====================================");
|
||||
|
||||
// Create a transaction
|
||||
let mut tx = Transaction::new();
|
||||
|
||||
// Add some operations
|
||||
let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64");
|
||||
let nano = Package::new("nano", "2.9.3-2", "amd64");
|
||||
|
||||
tx.add_install(vim).await?;
|
||||
tx.add_install(nano).await?;
|
||||
|
||||
println!("Created transaction with {} operations", tx.len());
|
||||
|
||||
// Resolve dependencies (this will validate the transaction)
|
||||
println!("Resolving dependencies...");
|
||||
tx.resolve().await?;
|
||||
println!("✓ Dependencies resolved successfully");
|
||||
|
||||
// IMPORTANT: The commit() method does NOT provide true atomicity
|
||||
println!("\n⚠️ IMPORTANT ATOMICITY NOTES:");
|
||||
println!(" • This transaction provides transaction-like semantics");
|
||||
println!(" • It does NOT provide true system-level atomicity");
|
||||
println!(" • If the operation fails midway, the system could be left in a corrupted state");
|
||||
println!(" • For true atomicity, apt-ostree must handle this at a higher level");
|
||||
println!(" • This might involve operating on a temporary directory and performing an atomic pivot_root");
|
||||
|
||||
println!("\nTransaction operations:");
|
||||
for (i, op) in tx.operations().iter().enumerate() {
|
||||
match op {
|
||||
apt_dnf_bridge::Operation::Install(pkg) => {
|
||||
println!(" {}: Install {}", i + 1, pkg.name);
|
||||
}
|
||||
apt_dnf_bridge::Operation::Remove(pkg) => {
|
||||
println!(" {}: Remove {}", i + 1, pkg.name);
|
||||
}
|
||||
apt_dnf_bridge::Operation::Upgrade(pkg) => {
|
||||
println!(" {}: Upgrade {}", i + 1, pkg.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nTo commit this transaction (commented out for safety):");
|
||||
println!(" tx.commit().await?;");
|
||||
|
||||
println!("\nFor apt-ostree integration, consider:");
|
||||
println!(" 1. Operating on a temporary directory");
|
||||
println!(" 2. Performing all APT operations there");
|
||||
println!(" 3. Using atomic pivot_root to switch to the new system state");
|
||||
println!(" 4. Rolling back if any step fails");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
105
examples/backend_selection.rs
Normal file
105
examples/backend_selection.rs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
//! Example demonstrating backend selection and switching.
|
||||
|
||||
use apt_dnf_bridge::{
|
||||
BackendConfig, Package, TransactionV2, PackageDatabaseV2,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("APT-DNF Bridge - Backend Selection Example");
|
||||
println!("======================================");
|
||||
|
||||
// 1. Auto-detect the best available backend
|
||||
println!("1. Auto-detecting best backend...");
|
||||
let tx = TransactionV2::new().await?;
|
||||
let (backend_name, backend_version) = tx.backend_info();
|
||||
println!(" Selected backend: {} v{}", backend_name, backend_version);
|
||||
|
||||
// 2. Create a transaction with shell backend explicitly
|
||||
println!("\n2. Creating transaction with shell backend...");
|
||||
let config = BackendConfig {
|
||||
enable_logging: true,
|
||||
use_cache: true,
|
||||
max_cache_size: 1000,
|
||||
command_timeout: Some(300),
|
||||
};
|
||||
let shell_tx = TransactionV2::with_shell_backend(config).await?;
|
||||
let (shell_name, shell_version) = shell_tx.backend_info();
|
||||
println!(" Shell backend: {} v{}", shell_name, shell_version);
|
||||
|
||||
// 3. Create a package database with mock backend for testing
|
||||
println!("\n3. Creating package database with mock backend...");
|
||||
let mock_config = BackendConfig {
|
||||
enable_logging: true,
|
||||
use_cache: true,
|
||||
max_cache_size: 100,
|
||||
command_timeout: Some(30),
|
||||
};
|
||||
let mut mock_db = PackageDatabaseV2::with_mock_backend(mock_config).await?;
|
||||
let (_mock_name, mock_version) = mock_db.backend_info();
|
||||
println!(" Mock backend: v{}", mock_version);
|
||||
|
||||
// 4. Demonstrate backend capabilities
|
||||
println!("\n4. Testing backend capabilities...");
|
||||
|
||||
// Test mock backend
|
||||
println!(" Testing mock backend:");
|
||||
let packages = mock_db.find_packages("vim").await?;
|
||||
println!(" Found {} packages matching 'vim'", packages.len());
|
||||
|
||||
if let Some(pkg_info) = mock_db.get_package_info("vim").await? {
|
||||
println!(" Package info: {} {}", pkg_info.name, pkg_info.version);
|
||||
}
|
||||
|
||||
// Test shell backend (if available)
|
||||
println!(" Testing shell backend:");
|
||||
let mut shell_db = PackageDatabaseV2::with_shell_backend(BackendConfig::default()).await?;
|
||||
let shell_packages = shell_db.find_packages("vim").await?;
|
||||
println!(" Found {} packages matching 'vim'", shell_packages.len());
|
||||
|
||||
// 5. Demonstrate transaction with different backends
|
||||
println!("\n5. Testing transactions with different backends...");
|
||||
|
||||
let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64");
|
||||
|
||||
// Mock transaction
|
||||
let mut mock_tx = TransactionV2::with_mock_backend(BackendConfig::default()).await?;
|
||||
mock_tx.add_install(vim.clone()).await?;
|
||||
println!(" Mock transaction: {} operations", mock_tx.len());
|
||||
|
||||
let resolution = mock_tx.resolve().await?;
|
||||
println!(" Mock resolution: resolvable={}, summary='{}'",
|
||||
resolution.resolvable, resolution.summary);
|
||||
|
||||
// Shell transaction (if available)
|
||||
let mut shell_tx = TransactionV2::with_shell_backend(BackendConfig::default()).await?;
|
||||
shell_tx.add_install(vim).await?;
|
||||
println!(" Shell transaction: {} operations", shell_tx.len());
|
||||
|
||||
let shell_resolution = shell_tx.resolve().await?;
|
||||
println!(" Shell resolution: resolvable={}, summary='{}'",
|
||||
shell_resolution.resolvable, shell_resolution.summary);
|
||||
|
||||
// 6. Show backend statistics
|
||||
println!("\n6. Backend statistics:");
|
||||
let mock_stats = mock_db.get_backend_stats().await?;
|
||||
println!(" Mock backend stats:");
|
||||
println!(" Commands executed: {}", mock_stats.commands_executed);
|
||||
println!(" Packages queried: {}", mock_stats.packages_queried);
|
||||
println!(" Cache hit rate: {:.2}%", mock_stats.cache_hit_rate * 100.0);
|
||||
|
||||
let shell_stats = shell_db.get_backend_stats().await?;
|
||||
println!(" Shell backend stats:");
|
||||
println!(" Commands executed: {}", shell_stats.commands_executed);
|
||||
println!(" Packages queried: {}", shell_stats.packages_queried);
|
||||
println!(" Cache hit rate: {:.2}%", shell_stats.cache_hit_rate * 100.0);
|
||||
|
||||
println!("\n✅ Backend selection example completed successfully!");
|
||||
println!("\n💡 Key benefits of pluggable backends:");
|
||||
println!(" • Start with simple shell backend (reliable)");
|
||||
println!(" • Switch to libapt backend when available (more powerful)");
|
||||
println!(" • Use mock backend for testing (fast, predictable)");
|
||||
println!(" • Same API works with all backends");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
54
examples/basic_usage.rs
Normal file
54
examples/basic_usage.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//! Basic usage example for the APT-DNF Bridge crate.
|
||||
|
||||
use apt_dnf_bridge::{Package, Repository, Transaction};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("APT-DNF Bridge - Basic Usage Example");
|
||||
println!("=================================");
|
||||
|
||||
// Create a package
|
||||
let vim = Package::new("vim", "2:8.1.2269-1ubuntu5.14", "amd64");
|
||||
println!("Created package: {}", vim.spec());
|
||||
|
||||
// Create a transaction with logging enabled
|
||||
let mut tx = Transaction::new_with_logging();
|
||||
println!("Created empty transaction with logging enabled");
|
||||
|
||||
// Add operations to the transaction
|
||||
tx.add_install(vim.clone()).await?;
|
||||
println!("Added install operation for: {}", vim.name);
|
||||
|
||||
// Check transaction status
|
||||
println!("Transaction has {} operations", tx.len());
|
||||
println!("Transaction is empty: {}", tx.is_empty());
|
||||
|
||||
// Note: In a real scenario, you would call:
|
||||
// tx.resolve().await?; // Resolve dependencies
|
||||
// tx.commit().await?; // Commit the transaction
|
||||
|
||||
// But for this example, we'll just show the structure
|
||||
println!("Transaction operations:");
|
||||
for (i, op) in tx.operations().iter().enumerate() {
|
||||
match op {
|
||||
apt_dnf_bridge::Operation::Install(pkg) => {
|
||||
println!(" {}: Install {}", i + 1, pkg.name);
|
||||
}
|
||||
apt_dnf_bridge::Operation::Remove(pkg) => {
|
||||
println!(" {}: Remove {}", i + 1, pkg.name);
|
||||
}
|
||||
apt_dnf_bridge::Operation::Upgrade(pkg) => {
|
||||
println!(" {}: Upgrade {}", i + 1, pkg.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a repository
|
||||
let mut repo = Repository::new("debian", "http://deb.debian.org/debian")?;
|
||||
repo.add_component("main");
|
||||
repo.add_component("contrib");
|
||||
println!("Created repository: {} at {}", repo.name, repo.url);
|
||||
println!("Components: {:?}", repo.components);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
60
examples/package_query.rs
Normal file
60
examples/package_query.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
//! Package querying example for the APT-DNF Bridge crate.
|
||||
|
||||
use apt_dnf_bridge::PackageDatabase;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("APT-DNF Bridge - Package Query Example");
|
||||
println!("===================================");
|
||||
|
||||
let mut db = PackageDatabase::new_with_logging();
|
||||
|
||||
// Search for packages
|
||||
println!("Searching for packages matching 'vim'...");
|
||||
let packages = db.find_packages("vim").await?;
|
||||
println!("Found {} packages:", packages.len());
|
||||
|
||||
for (i, pkg) in packages.iter().take(5).enumerate() {
|
||||
println!(" {}: {}", i + 1, pkg.name);
|
||||
}
|
||||
|
||||
if packages.len() > 5 {
|
||||
println!(" ... and {} more", packages.len() - 5);
|
||||
}
|
||||
|
||||
// Get detailed information about a specific package
|
||||
println!("\nGetting detailed info for 'vim'...");
|
||||
if let Some(pkg_info) = db.get_package_info("vim").await? {
|
||||
println!("Package: {}", pkg_info.name);
|
||||
println!("Version: {}", pkg_info.version);
|
||||
println!("Architecture: {}", pkg_info.architecture);
|
||||
if let Some(desc) = &pkg_info.description {
|
||||
println!("Description: {}", desc);
|
||||
}
|
||||
if let Some(size) = pkg_info.size {
|
||||
println!("Size: {} bytes", size);
|
||||
}
|
||||
} else {
|
||||
println!("Package 'vim' not found");
|
||||
}
|
||||
|
||||
// Check if a package is installed
|
||||
println!("\nChecking if 'vim' is installed...");
|
||||
let is_installed = db.is_installed("vim").await?;
|
||||
println!("vim is installed: {}", is_installed);
|
||||
|
||||
// List some installed packages
|
||||
println!("\nListing some installed packages...");
|
||||
let installed = db.get_installed_packages().await?;
|
||||
println!("Found {} installed packages", installed.len());
|
||||
|
||||
for (i, pkg) in installed.iter().take(10).enumerate() {
|
||||
println!(" {}: {} {}", i + 1, pkg.name, pkg.version);
|
||||
}
|
||||
|
||||
if installed.len() > 10 {
|
||||
println!(" ... and {} more", installed.len() - 10);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue