modelshelf 0.1.0

A shared local LLM model registry: discover, deduplicate, download, and update models across desktop apps.
Documentation
//! Undo: re-materialize independent copies from the store blob.

use super::{ActionOutcome, SkippedCopy, UndoReport};
use crate::paths::ShelfPaths;
use crate::registry::{LocationRole, RegistryStore};
use crate::{Error, Result};

/// Revert every applied action of a journaled operation. Each target gets a
/// private copy of the blob content again (copy + atomic rename), restoring
/// full pre-dedup independence.
pub(crate) fn undo(paths: &ShelfPaths, store: &RegistryStore, op_id: &str) -> Result<UndoReport> {
    let mut journal = super::execute::read_journal(paths, op_id)?;
    let mut report = UndoReport::default();

    for ja in &mut journal.actions {
        if ja.outcome != ActionOutcome::Applied {
            continue;
        }
        let action = &ja.action;
        let result = (|| -> Result<()> {
            if !action.blob.is_file() {
                return Err(Error::Refused(
                    "store blob no longer exists; cannot re-materialize".into(),
                ));
            }
            if !action.target.exists() {
                return Err(Error::Refused("target no longer exists".into()));
            }
            let tmp = super::execute::swap_tmp_path(&action.target);
            let _ = std::fs::remove_file(&tmp);
            // A copy fails cleanly with ENOSPC if the disk lacks room; the
            // original link stays untouched in that case.
            std::fs::copy(&action.blob, &tmp).map_err(|e| Error::io(&tmp, e))?;
            if let Err(e) = std::fs::rename(&tmp, &action.target) {
                let _ = std::fs::remove_file(&tmp);
                return Err(Error::io(&action.target, e));
            }
            Ok(())
        })();

        match result {
            Ok(()) => {
                ja.outcome = ActionOutcome::Undone;
                ja.reason = None;
                report.restored += 1;
                let target = action.target.clone();
                let id = action.id.clone();
                let _ = store.update(|reg| {
                    if let Some(entry) = reg.find_mut(&id) {
                        if let Some(loc) = entry.locations.iter_mut().find(|l| l.path == target) {
                            loc.role = LocationRole::External;
                        }
                    }
                    Ok(())
                });
            }
            Err(e) => report.skipped.push(SkippedCopy {
                path: action.target.clone(),
                reason: e.to_string(),
            }),
        }
    }

    super::execute::write_journal(paths, &journal)?;
    Ok(report)
}