//! Simple APT transaction implementation //! //! Provides a DNF-like transaction interface for APT operations. use std::process::Command; use anyhow::{Result, anyhow}; use std::collections::HashMap; /// Simple APT transaction that mimics DNF's imperative model pub struct AptTransaction { packages: Vec, before_versions: HashMap, // package -> version before after_versions: HashMap, // package -> version after } impl AptTransaction { /// Create a new transaction pub fn new() -> Result { Ok(Self { packages: Vec::new(), before_versions: HashMap::new(), after_versions: HashMap::new(), }) } /// Add a package to the transaction pub fn add_package(&mut self, name: &str) -> Result<()> { // Verify package exists let output = Command::new("apt") .args(&["show", name]) .output()?; if !output.status.success() { return Err(anyhow!("Package not found: {}", name)); } self.packages.push(name.to_string()); Ok(()) } /// Resolve dependencies (APT handles this automatically) pub fn resolve(&self) -> Result<()> { // APT handles dependency resolution automatically // Just validate all packages are available for package in &self.packages { let output = Command::new("apt") .args(&["show", package]) .output()?; if !output.status.success() { return Err(anyhow!("Package unavailable: {}", package)); } } Ok(()) } /// Commit the transaction pub fn commit(&mut self) -> Result<()> { if self.packages.is_empty() { return Ok(()); } // Get current versions before installation self.before_versions = self.get_package_versions()?; // Run apt install with all packages let output = Command::new("apt") .args(&["install", "-y"]) .args(&self.packages) .output()?; if !output.status.success() { let error = String::from_utf8_lossy(&output.stderr); return Err(anyhow!("APT installation failed: {}", error)); } // Get versions after installation self.after_versions = self.get_package_versions()?; Ok(()) } /// Rollback the transaction by restoring previous versions pub fn rollback(&self) -> Result<()> { if self.before_versions.is_empty() { return Ok(()); } // Build list of packages to restore to previous versions let mut packages_to_restore = Vec::new(); for (package, before_version) in &self.before_versions { if let Some(after_version) = self.after_versions.get(package) { // Only rollback if version changed if before_version != after_version { packages_to_restore.push(format!("{}={}", package, before_version)); } } else { // Package was newly installed, remove it packages_to_restore.push(format!("{}", package)); } } if packages_to_restore.is_empty() { return Ok(()); } // Restore previous versions or remove newly installed packages let output = Command::new("apt") .args(&["install", "-y"]) .args(&packages_to_restore) .output()?; if !output.status.success() { let error = String::from_utf8_lossy(&output.stderr); return Err(anyhow!("APT rollback failed: {}", error)); } Ok(()) } /// Get current package versions fn get_package_versions(&self) -> Result> { let output = Command::new("dpkg") .args(&["-l"]) .output()?; if !output.status.success() { return Err(anyhow!("Failed to get package versions")); } let mut versions = HashMap::new(); let output_str = String::from_utf8_lossy(&output.stdout); for line in output_str.lines() { if line.starts_with("ii") { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 3 { let package_name = parts[1]; let version = parts[2]; versions.insert(package_name.to_string(), version.to_string()); } } } Ok(versions) } /// Get list of packages in transaction pub fn packages(&self) -> &[String] { &self.packages } /// Check if transaction is empty pub fn is_empty(&self) -> bool { self.packages.is_empty() } /// Get list of packages that were changed by this transaction pub fn changed_packages(&self) -> Vec { let mut changed = Vec::new(); for (package, before_version) in &self.before_versions { if let Some(after_version) = self.after_versions.get(package) { if before_version != after_version { changed.push(format!("{}: {} -> {}", package, before_version, after_version)); } } else { changed.push(format!("{}: removed", package)); } } // Add newly installed packages for package in &self.packages { if !self.before_versions.contains_key(package) { if let Some(after_version) = self.after_versions.get(package) { changed.push(format!("{}: installed {}", package, after_version)); } } } changed } }