dotune 0.3.1

A cross-platform dotfile manager configured with Lua
use std::sync::Arc;

use super::abs_fs::{self, FsError};
use crate::model::{AbsolutePath, Command, CommandKind, SourcePath};

#[derive(Debug)]
pub enum ExecutionLog {
    DirectoryCreated(Arc<AbsolutePath>),
    PathDeleted(Arc<AbsolutePath>),
    SymlinkCreated {
        source_path: SourcePath,
        destination_path: Arc<AbsolutePath>,
    },
    SymlinkRecreated {
        source_path: SourcePath,
        destination_path: Arc<AbsolutePath>,
    },
    Copied {
        source_path: SourcePath,
        destination_path: Arc<AbsolutePath>,
    },
    Error {
        operation: &'static str,
        error: FsError,
        source_path: SourcePath,
        destination_path: Arc<AbsolutePath>,
    },
}

pub fn execute_commands(
    commands: impl IntoIterator<Item = Command>,
    dry_run: bool,
    mut log: impl FnMut(&AbsolutePath, ExecutionLog),
) {
    for command in commands {
        let mut command_log = |entry| log(&command.defined_in, entry);

        let (operation, source_path, destination_path, result) = match command.kind {
            CommandKind::Link {
                source_path,
                destination_path,
            } => {
                let result =
                    execute_link(&source_path, &destination_path, dry_run, &mut command_log);
                ("link", source_path, destination_path, result)
            }
            CommandKind::Copy {
                source_path,
                destination_path,
            } => {
                let result =
                    execute_copy(&source_path, &destination_path, dry_run, &mut command_log);
                ("copy", source_path, destination_path, result)
            }
        };
        if let Err(error) = result {
            command_log(ExecutionLog::Error {
                operation,
                error,
                source_path,
                destination_path,
            });
        }
    }
}

fn execute_link(
    source_path: &SourcePath,
    destination_path: &AbsolutePath,
    dry_run: bool,
    log: &mut impl FnMut(ExecutionLog),
) -> Result<(), FsError> {
    create_parent_dir(destination_path, dry_run, log)?;

    let replacing_symlink = if let Some(metadata) =
        abs_fs::symlink_metadata_if_exists(destination_path)?
        && metadata.is_symlink()
    {
        if !dry_run {
            abs_fs::remove_path(destination_path, metadata.file_type())?;
        }
        true
    } else {
        false
    };

    let create_result = source_path
        .metadata()
        .map_err(|error| FsError::Io(source_path.to_path_buf(), error))
        .and_then(|metadata| {
            if dry_run {
                Ok(())
            } else {
                abs_fs::create_symlink(source_path, destination_path, metadata.file_type())
            }
        });

    if let Err(error) = create_result {
        if !dry_run && replacing_symlink {
            log(ExecutionLog::PathDeleted(destination_path.into()));
        }
        return Err(error);
    }

    log(if replacing_symlink {
        ExecutionLog::SymlinkRecreated {
            source_path: source_path.clone(),
            destination_path: destination_path.into(),
        }
    } else {
        ExecutionLog::SymlinkCreated {
            source_path: source_path.clone(),
            destination_path: destination_path.into(),
        }
    });

    Ok(())
}

fn execute_copy(
    source_path: &SourcePath,
    destination_path: &AbsolutePath,
    dry_run: bool,
    log: &mut impl FnMut(ExecutionLog),
) -> Result<(), FsError> {
    create_parent_dir(destination_path, dry_run, log)?;
    if !dry_run {
        abs_fs::copy_overlay(source_path, destination_path)?;
    }
    log(ExecutionLog::Copied {
        source_path: source_path.clone(),
        destination_path: destination_path.into(),
    });
    Ok(())
}

fn create_parent_dir(
    destination_path: &AbsolutePath,
    dry_run: bool,
    log: &mut impl FnMut(ExecutionLog),
) -> Result<(), FsError> {
    let parent = if dry_run {
        destination_path.parent().filter(|parent| !parent.is_dir())
    } else {
        abs_fs::create_parent_dir(destination_path)?
    };
    if let Some(parent) = parent {
        log(ExecutionLog::DirectoryCreated(parent.into()));
    }
    Ok(())
}