apt-tx/src/transaction.rs
robojerk 7aaefb9957 Initial commit: apt-wrapper - Simple APT transaction wrapper
- Core transaction API with add_package, resolve, commit, rollback
- Version tracking for upgrades/downgrades
- Simple package info with FFI conversion functions
- Comprehensive error handling
- Basic test suite
- Clean, minimal implementation (~326 lines total)
2025-09-13 10:02:01 -07:00

184 lines
5.9 KiB
Rust

//! 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<String>,
before_versions: HashMap<String, String>, // package -> version before
after_versions: HashMap<String, String>, // package -> version after
}
impl AptTransaction {
/// Create a new transaction
pub fn new() -> Result<Self> {
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<HashMap<String, String>> {
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<String> {
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
}
}