//! Transaction management for apt-ostree daemon use crate::daemon::{DaemonResult, DaemonError}; use std::collections::HashMap; use std::fmt; use uuid::Uuid; use chrono::{DateTime, Utc}; /// Transaction types #[derive(Debug, Clone, PartialEq)] pub enum TransactionType { Install, Remove, Upgrade, Rollback, Deploy, Rebase, } /// Transaction states #[derive(Debug, Clone, PartialEq)] pub enum TransactionState { Pending, Running, Completed, Failed, Cancelled, } impl fmt::Display for TransactionState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TransactionState::Pending => write!(f, "pending"), TransactionState::Running => write!(f, "running"), TransactionState::Completed => write!(f, "completed"), TransactionState::Failed => write!(f, "failed"), TransactionState::Cancelled => write!(f, "cancelled"), } } } /// Transaction result #[derive(Debug, Clone)] pub struct TransactionResult { pub success: bool, pub message: String, pub details: Option, } /// Transaction information #[derive(Debug, Clone)] pub struct Transaction { pub id: String, pub transaction_type: TransactionType, pub state: TransactionState, pub created_at: DateTime, pub started_at: Option>, pub completed_at: Option>, pub result: Option, pub progress: f64, // 0.0 to 1.0 pub metadata: HashMap, } impl Transaction { pub fn new(transaction_type: TransactionType) -> Self { Self { id: Uuid::new_v4().to_string(), transaction_type, state: TransactionState::Pending, created_at: Utc::now(), started_at: None, completed_at: None, result: None, progress: 0.0, metadata: HashMap::new(), } } pub fn start(&mut self) -> DaemonResult<()> { if self.state != TransactionState::Pending { return Err(DaemonError::Transaction("Transaction is not in pending state".to_string())); } self.state = TransactionState::Running; self.started_at = Some(Utc::now()); Ok(()) } pub fn complete(&mut self, result: TransactionResult) -> DaemonResult<()> { if self.state != TransactionState::Running { return Err(DaemonError::Transaction("Transaction is not in running state".to_string())); } self.state = TransactionState::Completed; self.completed_at = Some(Utc::now()); self.result = Some(result); self.progress = 1.0; Ok(()) } pub fn fail(&mut self, error: String) -> DaemonResult<()> { if self.state != TransactionState::Running { return Err(DaemonError::Transaction("Transaction is not in running state".to_string())); } self.state = TransactionState::Failed; self.completed_at = Some(Utc::now()); self.result = Some(TransactionResult { success: false, message: error, details: None, }); Ok(()) } pub fn cancel(&mut self) -> DaemonResult<()> { if self.state != TransactionState::Pending && self.state != TransactionState::Running { return Err(DaemonError::Transaction("Transaction cannot be cancelled in current state".to_string())); } self.state = TransactionState::Cancelled; self.completed_at = Some(Utc::now()); self.result = Some(TransactionResult { success: false, message: "Transaction cancelled".to_string(), details: None, }); Ok(()) } pub fn update_progress(&mut self, progress: f64) -> DaemonResult<()> { if !(0.0..=1.0).contains(&progress) { return Err(DaemonError::Transaction("Progress must be between 0.0 and 1.0".to_string())); } self.progress = progress; Ok(()) } pub fn add_metadata(&mut self, key: String, value: String) { self.metadata.insert(key, value); } } /// Transaction manager #[allow(dead_code, clippy::new_without_default)] pub struct TransactionManager { transactions: HashMap, next_transaction_id: u64, } impl TransactionManager { pub fn new() -> Self { Self { transactions: HashMap::new(), next_transaction_id: 1, } } pub fn start_transaction(&mut self, transaction_type: TransactionType) -> String { let transaction = Transaction::new(transaction_type); let id = transaction.id.clone(); self.transactions.insert(id.clone(), transaction); id } pub fn get_transaction(&self, transaction_id: &str) -> Option<&Transaction> { self.transactions.get(transaction_id) } pub fn get_transaction_mut(&mut self, transaction_id: &str) -> Option<&mut Transaction> { self.transactions.get_mut(transaction_id) } pub fn get_transaction_status(&self, transaction_id: &str) -> DaemonResult { let transaction = self.transactions.get(transaction_id) .ok_or_else(|| DaemonError::Transaction(format!("Transaction {} not found", transaction_id)))?; Ok(transaction.state.clone()) } pub fn list_transactions(&self) -> Vec<&Transaction> { self.transactions.values().collect() } pub fn cleanup_completed_transactions(&mut self, max_age_hours: u64) { let cutoff = Utc::now() - chrono::Duration::hours(max_age_hours as i64); self.transactions.retain(|_, transaction| { if let Some(completed_at) = transaction.completed_at { completed_at > cutoff } else { true // Keep incomplete transactions } }); } pub fn get_active_transactions(&self) -> Vec<&Transaction> { self.transactions.values() .filter(|t| t.state == TransactionState::Pending || t.state == TransactionState::Running) .collect() } pub fn start_existing_transaction(&mut self, transaction_id: &str) -> DaemonResult<()> { if let Some(transaction) = self.transactions.get_mut(transaction_id) { transaction.start()?; } Ok(()) } pub fn complete_transaction(&mut self, transaction_id: &str, success: bool, message: String) -> DaemonResult<()> { if let Some(transaction) = self.transactions.get_mut(transaction_id) { let result = TransactionResult { success, message: message.clone(), details: None, }; if success { transaction.complete(result)?; } else { transaction.fail(message)?; } } Ok(()) } pub fn get_active_transaction_count(&self) -> usize { self.transactions.values() .filter(|t| t.state == TransactionState::Running || t.state == TransactionState::Pending) .count() } } impl Default for TransactionManager { fn default() -> Self { Self::new() } }