provenance-cli 0.2.1

Command-line interface for Provenance requirements traceability
use super::{PublicationLock, PublishError, StageIdentity, TransactionDirectory, TransactionPaths};
use camino::Utf8Path;
use remove_dir_all::RemoveDir;
use std::fs::File;

impl PublicationLock {
    pub(in crate::wiki::publish) fn cleanup(
        self,
        transaction: &TransactionDirectory,
    ) -> std::io::Result<()> {
        transaction.rename(&transaction.leaves.lock, &transaction.leaves.lock_cleanup)?;
        if transaction.child_identity(&transaction.leaves.lock_cleanup)? != self.identity {
            let mismatch = std::io::Error::other("publication lock path changed before cleanup");
            return match transaction
                .rename(&transaction.leaves.lock_cleanup, &transaction.leaves.lock)
            {
                Ok(()) => Err(mismatch),
                Err(restore) => Err(std::io::Error::other(format!(
                    "{mismatch}; restoring the replacement lock path also failed: {restore}"
                ))),
            };
        }
        drop(self);
        transaction.remove_file(&transaction.leaves.lock_cleanup)
    }
}

impl TransactionDirectory {
    pub(in crate::wiki::publish) fn remove_stage(
        &self,
        mut stage: File,
        expected: &StageIdentity,
    ) -> std::io::Result<()> {
        self.rename(&self.leaves.stage, &self.leaves.stage_cleanup)?;
        if self.child_identity(&self.leaves.stage_cleanup)? != expected.0 {
            let mismatch = std::io::Error::other("staging directory path changed before cleanup");
            return match self.rename(&self.leaves.stage_cleanup, &self.leaves.stage) {
                Ok(()) => Err(mismatch),
                Err(restore) => Err(std::io::Error::other(format!(
                    "{mismatch}; restoring the replacement staging directory also failed: {restore}"
                ))),
            };
        }
        stage.remove_dir_contents(Some(self.paths.stage_cleanup.as_std_path()))?;
        drop(stage);
        match fs_at::OpenOptions::default().rmdir_at(&self.parent, &self.leaves.stage_cleanup) {
            Ok(()) => Ok(()),
            Err(cleanup) => match self.rename(&self.leaves.stage_cleanup, &self.leaves.stage) {
                Ok(()) => Err(cleanup),
                Err(restore) => Err(std::io::Error::other(format!(
                    "{cleanup}; restoring the staging directory after cleanup failed: {restore}"
                ))),
            },
        }
    }
}

pub(super) fn rollback_validation_failure(
    output: &Utf8Path,
    paths: &TransactionPaths,
    validation: &PublishError,
    rename: &mut impl FnMut(&Utf8Path, &Utf8Path) -> std::io::Result<()>,
) -> PublishError {
    match rename(&paths.backup, output) {
        Ok(()) => PublishError::OutputChanged {
            path: output.to_path_buf(),
            detail: validation.to_string(),
        },
        Err(rollback) => PublishError::RollbackFailed {
            output: output.to_path_buf(),
            backup: paths.backup.clone(),
            install: std::io::Error::other(validation.to_string()),
            rollback,
        },
    }
}

pub(super) fn rollback_install_failure(
    output: &Utf8Path,
    paths: &TransactionPaths,
    install: std::io::Error,
    rename: &mut impl FnMut(&Utf8Path, &Utf8Path) -> std::io::Result<()>,
) -> PublishError {
    match rename(&paths.backup, output) {
        Ok(()) => PublishError::ReplacementRolledBack {
            output: output.to_path_buf(),
            source: install,
        },
        Err(rollback) => PublishError::RollbackFailed {
            output: output.to_path_buf(),
            backup: paths.backup.clone(),
            install,
            rollback,
        },
    }
}

pub(super) fn verify_installed_stage_with_backup(
    stage_identity: &StageIdentity,
    output: &Utf8Path,
    transaction: &TransactionDirectory,
    rename: &mut impl FnMut(&Utf8Path, &Utf8Path) -> std::io::Result<()>,
) -> Result<(), PublishError> {
    let paths = &transaction.paths;
    let Err(validation) = super::replacement::verify_stage_identity(
        stage_identity,
        transaction,
        &transaction.output_leaf,
        output,
        "installed output does not match the generated staging directory",
    ) else {
        return Ok(());
    };
    let install = std::io::Error::other(validation.to_string());
    if let Err(quarantine) = rename(output, &paths.stage) {
        return Err(PublishError::RollbackFailed {
            output: output.to_path_buf(),
            backup: paths.backup.clone(),
            install,
            rollback: quarantine,
        });
    }
    match rename(&paths.backup, output) {
        Ok(()) => Err(validation),
        Err(rollback) => Err(PublishError::RollbackFailed {
            output: output.to_path_buf(),
            backup: paths.backup.clone(),
            install,
            rollback,
        }),
    }
}

pub(super) fn cleanup_backup(
    transaction: &TransactionDirectory,
    before_cleanup: &mut impl FnMut(&camino::Utf8Path) -> std::io::Result<()>,
) -> std::io::Result<()> {
    let backup_path = &transaction.paths.backup;
    let mut backup = transaction.open_dir(&transaction.leaves.backup)?;
    let expected_identity = same_file::Handle::from_file(backup.try_clone()?)?;
    before_cleanup(backup_path)?;
    if transaction.child_identity(&transaction.leaves.backup)? != expected_identity {
        return Err(std::io::Error::other("backup path changed before cleanup"));
    }

    backup.remove_dir_contents(Some(backup_path.as_std_path()))?;
    drop(backup);
    fs_at::OpenOptions::default().rmdir_at(&transaction.parent, &transaction.leaves.backup)
}