From fded317a65dc91105363ac2fcf140caa5c40037c Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Mon, 27 Dec 2021 08:31:56 -0500 Subject: [PATCH] Handle collisions with existing configs + Backup all conflicts to the backup directory set through CLI + Resolves any symlinks to backup their destination files or paths --- src/kot.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++-- src/kot/fs.rs | 56 ++++++++++++----------------------------- 2 files changed, 83 insertions(+), 42 deletions(-) diff --git a/src/kot.rs b/src/kot.rs index c5a8f81..e70ad9b 100644 --- a/src/kot.rs +++ b/src/kot.rs @@ -6,6 +6,9 @@ ## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## ##############################################################################*/ +use std::path::PathBuf; +use crate::kot::fs::check_collisions; + pub mod cli; pub mod fs; pub mod io; @@ -18,15 +21,18 @@ pub mod io; // Creates symbolic links to the configurations we're installing // TODO: On error, revert to last good state +// TODO: User script to execute after installing configs successfully +// TODO: Function to uninstall configs. Loop through dotfiles and restore backup files or delete configs pub fn install_configs(args: & cli::Cli) -> std::io::Result<()> { // Get the configurations and their target installation paths // + Checks for conflicts and prompts user to abort or continue let config_map = fs::get_target_paths(&args)?; + handle_collisions(&args, &config_map)?; // At this point there are either no conflicts or the user agreed to them + println!("Installing configs:"); for (config_path, target_path) in &config_map { - println!("Installing config: {:?}\n+ At location: {:?}\n", config_path, target_path); - + println!(" + {:?}", target_path); match std::os::unix::fs::symlink(config_path, target_path) { Ok(()) => (), // Configuration installed successfully Err(_e) => { @@ -45,3 +51,62 @@ pub fn install_configs(args: & cli::Cli) -> std::io::Result<()> { } Ok(()) } + +fn handle_collisions(args : & cli::Cli, + config_map : & fs::HashMap) -> io::Result<()> { + let conflicts = check_collisions(&config_map) + .expect("Error: Failed to check collisions"); + + // If we found collisions in the configurations + if &conflicts.len() > &0 { + // Ask client if they would like to abort given the config collisions + let mut msg = format!("The following configurations already exist:"); + for config in conflicts.iter() { + msg += format!("\n {:?}", config).as_str(); + } + msg += format!("\nIf you continue, backups will be made in {:?}. \ + Any configurations there will be overwritten.\ + \nAbort? Enter y/n or Y/N: ", &args.backup_dir).as_str(); + + // If we abort, exit; If we continue, back up the configs + match io::prompt(msg) { + true => return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)), + false => { + // Backup each conflicting config at the install location + for backup_target in conflicts.iter() { + backup_config(backup_target, &args.backup_dir) + .expect(format!("Error: Unable to backup config: {:?}", backup_target) + .as_str()) + } + }, + }; + }; + Ok(()) +} + +// Creates a backup of configurations that conflict +// + Backup directory location is specified by CLI --backup-dir +// TODO: Automatically create backup directory +// TODO: .kotignore in dotfiles repo to specify files to not install / backup +// TODO: .kotrc in dotfiles repo or home dir to set backup-dir and install-dir? +fn backup_config(config_path: & fs::PathBuf, backup_dir: & fs::PathBuf) -> io::Result<()> { + let mut backup_path = backup_dir.to_owned(); + backup_path.push(config_path.file_name().unwrap()); + match config_path.is_dir() { + true => { + // Copy directory with recursion using fs_extra::dir::move_dir + let mut options = fs::dir::CopyOptions::new(); + options.copy_inside = true; + // TODO: Add a flag to overwrite backups, otherwise warn and abort + options.overwrite = true; + fs::dir::move_dir(config_path, backup_path, &options) + }, + false => { + // Copy single configuration file + let mut options = fs_extra::file::CopyOptions::new(); + options.overwrite = true; + fs_extra::file::move_file(config_path, backup_path, &options) + }, + }.expect(&format!("Error: Unable to backup config: {:?}", config_path)); + Ok(()) +} diff --git a/src/kot/fs.rs b/src/kot/fs.rs index bd9ea3d..9b222de 100644 --- a/src/kot/fs.rs +++ b/src/kot/fs.rs @@ -9,9 +9,9 @@ // Allow the use of kot::fs::Path and kot::fs::PathBuf from std::path:: pub use std::path::{Path, PathBuf}; pub use std::collections::HashMap; +pub use fs_extra::dir; use std::fs; -use fs_extra::dir; // ============================================================================= // IMPLEMENTATION @@ -19,32 +19,9 @@ use fs_extra::dir; // ----------------------------------------------------------------------------- -// Creates a backup of configurations that conflict -// + Backup directory location is specified by CLI --backup-dir -// TODO: Automatically create backup directory -// TODO: .kotignore in dotfiles repo to specify files to not install / backup -// TODO: .kotrc in dotfiles repo or home dir to set backup-dir and install-dir? -fn backup_config(config_path: & PathBuf, backup_dir: & PathBuf) -> super::io::Result<()> { - let mut backup_path = backup_dir.to_owned(); - backup_path.push(config_path.file_name().unwrap()); - match config_path.is_dir() { - true => { - // Copy directory with recursion using fs_extra::dir::move_dir - let mut options = dir::CopyOptions::new(); - options.copy_inside = true; - dir::move_dir(config_path, backup_path, &options) - }, - false => { - // Copy single configuration file - let options = fs_extra::file::CopyOptions::new(); - fs_extra::file::move_file(config_path, backup_path, &options) - }, - }.expect(&format!("Error: Unable to backup config: {:?}", config_path)); - Ok(()) -} - // Initialize and return a HashMap // Later used to check each install location for conflicts before installing +// This function does not create or modify any files or directories pub fn get_target_paths(args: & super::cli::Cli) -> super::io::Result> { let mut config_map = HashMap::new(); @@ -59,20 +36,9 @@ pub fn get_target_paths(args: & super::cli::Cli) -> super::io::Result return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)), - false => backup_config(&config_target, &args.backup_dir).ok(), - }; - }; - // If the entry doesn't already exist, insert it into the config_map + // Key is full path to source config from dotfiles repo we're installing + // Value is desired full path to config at final install location // TODO: If the entry does exist, should there be an exception? config_map.entry(entry.path().to_owned()) .or_insert(config_target.to_owned()); @@ -81,8 +47,18 @@ pub fn get_target_paths(args: & super::cli::Cli) -> super::io::Result) -> super::io::Result> { + let mut config_conflicts = vec![]; + for (_path, target_config) in config_map.iter() { + // If the target configuration file or directory already exists + if target_config.exists() { + config_conflicts.push(target_config.to_owned()); + } + } + Ok(config_conflicts) +} +