zoi-rs 1.7.0

Universal Package Manager & Environment Setup Tool
Documentation
use crate::pkg::{audit, install, types, uninstall};
use anyhow::{Result, anyhow};
use chrono::Utc;
use colored::*;
use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
use uuid::{Timestamp, Uuid};

fn get_transactions_dir() -> Result<PathBuf> {
    let home_dir = home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
    let dir = home_dir.join(".zoi").join("transactions");
    fs::create_dir_all(&dir)?;
    Ok(dir)
}

fn get_transaction_path(id: &str) -> Result<PathBuf> {
    Ok(get_transactions_dir()?.join(format!("{}.json", id)))
}

pub fn begin() -> Result<types::Transaction> {
    let transaction = types::Transaction {
        id: Uuid::new_v7(Timestamp::from_unix(
            uuid::NoContext,
            Utc::now().timestamp_millis() as u64,
            0,
        ))
        .to_string(),
        start_time: Utc::now().to_rfc3339(),
        operations: Vec::new(),
    };
    let path = get_transaction_path(&transaction.id)?;
    let content = serde_json::to_string_pretty(&transaction)?;
    fs::write(path, content)?;
    Ok(transaction)
}

pub fn record_operation(
    transaction_id: &str,
    operation: types::TransactionOperation,
) -> Result<()> {
    match &operation {
        types::TransactionOperation::Install { manifest } => {
            let _ = audit::log_event(audit::AuditAction::Install, manifest);
        }
        types::TransactionOperation::Uninstall { manifest } => {
            let _ = audit::log_event(audit::AuditAction::Uninstall, manifest);
        }
        types::TransactionOperation::Upgrade { new_manifest, .. } => {
            let _ = audit::log_event(audit::AuditAction::Upgrade, new_manifest);
        }
    }

    let path = get_transaction_path(transaction_id)?;
    let content = fs::read_to_string(&path)?;
    let mut transaction: types::Transaction = serde_json::from_str(&content)?;
    transaction.operations.push(operation);
    let new_content = serde_json::to_string_pretty(&transaction)?;
    fs::write(path, new_content)?;
    Ok(())
}

pub fn commit(transaction_id: &str) -> Result<()> {
    delete_log(transaction_id)
}

pub fn get_modified_files(transaction_id: &str) -> Result<Vec<String>> {
    let path = get_transaction_path(transaction_id)?;
    if !path.exists() {
        return Ok(Vec::new());
    }
    let content = fs::read_to_string(&path)?;
    let transaction: types::Transaction = serde_json::from_str(&content)?;

    let mut files = HashSet::new();
    for op in transaction.operations {
        match op {
            types::TransactionOperation::Install { manifest } => {
                for file in manifest.installed_files {
                    files.insert(file);
                }
            }
            types::TransactionOperation::Uninstall { manifest } => {
                for file in manifest.installed_files {
                    files.insert(file);
                }
            }
            types::TransactionOperation::Upgrade {
                old_manifest,
                new_manifest,
            } => {
                for file in old_manifest.installed_files {
                    files.insert(file);
                }
                for file in new_manifest.installed_files {
                    files.insert(file);
                }
            }
        }
    }
    Ok(files.into_iter().collect())
}

pub fn delete_log(transaction_id: &str) -> Result<()> {
    let path = get_transaction_path(transaction_id)?;
    if path.exists() {
        fs::remove_file(path)?;
    }
    Ok(())
}

pub fn rollback(transaction_id: &str) -> Result<()> {
    let path = get_transaction_path(transaction_id)?;
    if !path.exists() {
        return Err(anyhow!(
            "Transaction log not found for ID: {}",
            transaction_id
        ));
    }
    let content = fs::read_to_string(&path)?;
    let transaction: types::Transaction = serde_json::from_str(&content)?;

    println!("\n{}", "--- Starting Rollback ---".yellow().bold());

    for operation in transaction.operations.iter().rev() {
        match operation {
            types::TransactionOperation::Install { manifest } => {
                println!(
                    "Rolling back installation of {} v{}...",
                    manifest.name.cyan(),
                    manifest.version.yellow()
                );
                if let Err(e) = uninstall::run(&manifest.name, Some(manifest.scope), true) {
                    eprintln!(
                        "{} Failed to rollback install of '{}': {}",
                        "Error:".red().bold(),
                        manifest.name,
                        e
                    );
                }
            }
            types::TransactionOperation::Uninstall { manifest } => {
                println!(
                    "Rolling back uninstallation of {} v{}...",
                    manifest.name.cyan(),
                    manifest.version.yellow()
                );
                let source = format!(
                    "#{}@{}/{}@{}",
                    manifest.registry_handle, manifest.repo, manifest.name, manifest.version
                );
                let (graph, _) = match install::resolver::resolve_dependency_graph(
                    &[source],
                    Some(manifest.scope),
                    true,
                    true,
                    true,
                    None,
                    true,
                ) {
                    Ok(res) => res,
                    Err(e) => {
                        eprintln!(
                            "{} Failed to resolve dependency graph for rollback of '{}': {}",
                            "Error:".red().bold(),
                            manifest.name,
                            e
                        );
                        continue;
                    }
                };

                let install_plan = match install::plan::create_install_plan(&graph.nodes) {
                    Ok(plan) => plan,
                    Err(e) => {
                        eprintln!(
                            "{} Failed to create install plan for rollback of '{}': {}",
                            "Error:".red().bold(),
                            manifest.name,
                            e
                        );
                        continue;
                    }
                };

                let stages = match graph.toposort() {
                    Ok(s) => s,
                    Err(e) => {
                        eprintln!(
                            "{} Failed to sort dependency graph for rollback of '{}': {}",
                            "Error:".red().bold(),
                            manifest.name,
                            e
                        );
                        continue;
                    }
                };

                for stage in stages {
                    for id in stage {
                        let node = graph.nodes.get(&id).unwrap();
                        if let Some(action) = install_plan.get(&id)
                            && let Err(e) = install::installer::install_node(
                                node, action, None, None, true, true,
                            )
                        {
                            eprintln!(
                                "{} Failed to re-install during rollback of '{}': {}",
                                "Error:".red().bold(),
                                manifest.name,
                                e
                            );
                        }
                    }
                }
            }
            types::TransactionOperation::Upgrade {
                old_manifest,
                new_manifest,
            } => {
                println!(
                    "Rolling back upgrade of {} from {} to {}...",
                    old_manifest.name.cyan(),
                    new_manifest.version.yellow(),
                    old_manifest.version.green()
                );
                if let Err(e) = uninstall::run(&new_manifest.name, Some(new_manifest.scope), true) {
                    eprintln!(
                        "{} Failed to uninstall new version during upgrade-rollback for '{}': {}",
                        "Error:".red().bold(),
                        new_manifest.name,
                        e
                    );
                }
                let source = format!(
                    "#{}@{}/{}@{}",
                    old_manifest.registry_handle,
                    old_manifest.repo,
                    old_manifest.name,
                    old_manifest.version
                );
                let (graph, _) = match install::resolver::resolve_dependency_graph(
                    std::slice::from_ref(&source),
                    Some(old_manifest.scope),
                    true,
                    true,
                    true,
                    None,
                    true,
                ) {
                    Ok(res) => res,
                    Err(e) => {
                        eprintln!(
                            "{} Failed to resolve dependency graph for rollback of '{}': {}",
                            "Error:".red().bold(),
                            old_manifest.name,
                            e
                        );
                        continue;
                    }
                };

                let install_plan = match install::plan::create_install_plan(&graph.nodes) {
                    Ok(plan) => plan,
                    Err(e) => {
                        eprintln!(
                            "{} Failed to create install plan for rollback of '{}': {}",
                            "Error:".red().bold(),
                            old_manifest.name,
                            e
                        );
                        continue;
                    }
                };

                let stages = match graph.toposort() {
                    Ok(s) => s,
                    Err(e) => {
                        eprintln!(
                            "{} Failed to sort dependency graph for rollback of '{}': {}",
                            "Error:".red().bold(),
                            old_manifest.name,
                            e
                        );
                        continue;
                    }
                };

                for stage in stages {
                    for id in stage {
                        let node = graph.nodes.get(&id).unwrap();
                        if let Some(action) = install_plan.get(&id)
                            && let Err(e) = install::installer::install_node(
                                node, action, None, None, true, true,
                            )
                        {
                            eprintln!(
                                "{} Failed to re-install during rollback of '{}': {}",
                                "Error:".red().bold(),
                                old_manifest.name,
                                e
                            );
                        }
                    }
                }
            }
        }
    }

    println!("{}", "--- Rollback Complete ---".yellow().bold());
    delete_log(transaction_id)?;
    Ok(())
}

pub fn get_last_transaction_id() -> Result<Option<String>> {
    let dir = get_transactions_dir()?;
    let mut last_modified_time = None;
    let mut last_transaction_id = None;

    if !dir.exists() {
        return Ok(None);
    }

    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("json") {
            let metadata = fs::metadata(&path)?;
            let modified_time = metadata.modified()?;

            if last_modified_time.is_none() || modified_time > last_modified_time.unwrap() {
                last_modified_time = Some(modified_time);
                last_transaction_id = path.file_stem().and_then(|s| s.to_str()).map(String::from);
            }
        }
    }

    Ok(last_transaction_id)
}