Add enhanced rollback functionality and comprehensive documentation

- Implement two-phase rollback system inspired by apt-tx project
- Add package state tracking (newly_installed, upgraded, previously_installed)
- Enhance both core and advanced transaction APIs with rollback methods
- Add comprehensive rollback documentation in docs/rollback.md
- Create rollback_demo.rs example demonstrating functionality
- Update README with detailed crate usage instructions
- Add rollback features to feature flags documentation
- Fix import issues in advanced crate modules
- Add get_installed_packages method to AptCommands
- Include both crates.io and git installation options in README
This commit is contained in:
robojerk 2025-09-13 21:02:29 -07:00
parent 06cafa0366
commit 88d57cd3a1
15 changed files with 945 additions and 47 deletions

View file

@ -13,3 +13,4 @@ anyhow.workspace = true
serde.workspace = true
thiserror.workspace = true
tokio.workspace = true

View file

@ -137,6 +137,32 @@ impl AptCommands {
cmd
}
/// Get list of installed packages as a future.
pub async fn get_installed_packages() -> Result<Vec<String>> {
let output = Self::list_installed().output()?;
if !output.status.success() {
return Err(AptError::CommandFailed {
command: String::from_utf8_lossy(&output.stderr).to_string()
});
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut packages = Vec::new();
for line in stdout.lines() {
if line.starts_with("ii ") {
// dpkg -l output format: ii package version arch description
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
packages.push(parts[1].to_string());
}
}
}
Ok(packages)
}
}
/// Validate package names to prevent injection.

View file

@ -13,3 +13,4 @@ pub use error::{AptError, Result};
pub use package::{Package, PackageDatabase};
pub use repository::{Repository, RepositoryManager};
pub use transaction::{Operation, Transaction};

View file

@ -2,7 +2,7 @@
use crate::{AptError, Package, Result, command::{AptCommands, validate_package_name}};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::collections::{HashSet, HashMap};
/// Represents an operation in a transaction.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -22,6 +22,10 @@ pub struct Transaction {
operations: Vec<Operation>,
/// Packages that were installed before this transaction (for rollback).
previously_installed: HashSet<String>,
/// Packages that were newly installed in this transaction (for rollback).
newly_installed: HashSet<String>,
/// Packages that were upgraded in this transaction (package -> old_version).
upgraded: HashMap<String, String>,
/// Whether to enable logging.
log_commands: bool,
}
@ -32,6 +36,8 @@ impl Transaction {
Self {
operations: Vec::new(),
previously_installed: HashSet::new(),
newly_installed: HashSet::new(),
upgraded: HashMap::new(),
log_commands: false,
}
}
@ -41,6 +47,8 @@ impl Transaction {
Self {
operations: Vec::new(),
previously_installed: HashSet::new(),
newly_installed: HashSet::new(),
upgraded: HashMap::new(),
log_commands: true,
}
}
@ -211,6 +219,9 @@ impl Transaction {
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
// Track what was actually installed/upgraded for rollback
self.track_committed_changes().await?;
}
Ok(())
@ -218,17 +229,60 @@ impl Transaction {
/// 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
// Clear previous state
self.previously_installed.clear();
self.newly_installed.clear();
self.upgraded.clear();
// Get currently installed packages
let installed_packages = AptCommands::get_installed_packages().await?;
for package in installed_packages {
self.previously_installed.insert(package);
}
Ok(())
}
/// Track what packages were actually installed/upgraded after commit.
async fn track_committed_changes(&mut self) -> Result<()> {
// Get current installed packages
let current_installed = AptCommands::get_installed_packages().await?;
let current_set: HashSet<String> = current_installed.into_iter().collect();
// Find newly installed packages
for package in &current_set {
if !self.previously_installed.contains(package as &str) {
self.newly_installed.insert(package.clone());
}
}
// For upgrades, we need to check if packages were upgraded
// This is a simplified approach - in practice, you'd need to track
// the specific versions that were upgraded
for operation in &self.operations {
match operation {
Operation::Upgrade(pkg) => {
if current_set.contains(&pkg.name) {
// Mark as upgraded (we don't have old version info easily available)
self.upgraded.insert(pkg.name.clone(), "unknown".to_string());
}
}
_ => {}
}
}
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.
/// This method attempts to restore the system to its previous state by:
/// 1. Removing newly installed packages
/// 2. Downgrading upgraded packages (if possible)
///
/// Note: This is a best-effort implementation and cannot guarantee complete
/// rollback due to APT's limitations. For critical systems, consider using
/// OSTree's native checkpoint/rollback functionality.
pub async fn rollback(&self) -> Result<()> {
if self.previously_installed.is_empty() {
return Err(AptError::Generic(anyhow::anyhow!(
@ -236,11 +290,57 @@ impl Transaction {
)));
}
// 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"
)))
// Step 1: Remove newly installed packages
if !self.newly_installed.is_empty() {
let packages_to_remove: Vec<&str> = self.newly_installed.iter().map(|s| s.as_str()).collect();
let mut cmd = AptCommands::remove(&packages_to_remove);
if self.log_commands {
cmd = cmd.with_logging();
}
let output = cmd.output()?;
if !output.status.success() {
return Err(AptError::Generic(anyhow::anyhow!(
"Failed to remove newly installed packages during rollback: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
}
// Step 2: Attempt to downgrade upgraded packages
// Note: This is complex with APT as we need to specify exact versions
// For now, we'll just log what would need to be downgraded
if !self.upgraded.is_empty() {
if self.log_commands {
println!("Warning: The following packages were upgraded and may need manual downgrade:");
for (package, old_version) in &self.upgraded {
println!(" {} (was version: {})", package, old_version);
}
}
}
Ok(())
}
/// Get packages that were newly installed in this transaction.
pub fn newly_installed(&self) -> &HashSet<String> {
&self.newly_installed
}
/// Get packages that were upgraded in this transaction.
pub fn upgraded(&self) -> &HashMap<String, String> {
&self.upgraded
}
/// Get packages that were previously installed before this transaction.
pub fn previously_installed(&self) -> &HashSet<String> {
&self.previously_installed
}
/// Check if the transaction has any changes that can be rolled back.
pub fn can_rollback(&self) -> bool {
!self.newly_installed.is_empty() || !self.upgraded.is_empty()
}
/// Clear all operations from the transaction.