From 069f5cc1283c616b401bab6f9b5db57c41bf1642 Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Wed, 29 Dec 2021 13:31:09 -0500 Subject: [PATCH] Improve error handling + Alias for returning Result with varying Error types + Update all Result returns to use new alias + Documentation comments for functions and structs + Add move_dir and move_file functions to fs wrapper module + Add `--force` flag for overwriting an existing configuration backup --- src/kot.rs | 102 +++++++++++++++++++++++++++++-------------------- src/kot/cli.rs | 58 ++++++++++++++++++++++------ src/kot/fs.rs | 70 ++++++++++++++++++++++----------- src/kot/io.rs | 4 +- src/main.rs | 23 ++++++++--- 5 files changed, 174 insertions(+), 83 deletions(-) diff --git a/src/kot.rs b/src/kot.rs index e70ad9b..1072b9d 100644 --- a/src/kot.rs +++ b/src/kot.rs @@ -13,20 +13,24 @@ pub mod cli; pub mod fs; pub mod io; +/// Result alias to return result with Error of various types +pub type Result = std::result::Result>; + // ============================================================================= // IMPLEMENTATION // ============================================================================= // ----------------------------------------------------------------------------- -// Creates symbolic links to the configurations we're installing +/// 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<()> { +pub fn install_configs(args: & cli::Cli) -> 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)?; + + // Check if there are any existing files in the install directory that are also within the dotfiles to install handle_collisions(&args, &config_map)?; // At this point there are either no conflicts or the user agreed to them @@ -36,7 +40,7 @@ pub fn install_configs(args: & cli::Cli) -> std::io::Result<()> { match std::os::unix::fs::symlink(config_path, target_path) { Ok(()) => (), // Configuration installed successfully Err(_e) => { - // Attempt to remove the file or directory, and then symlink the new config + // Attempt to remove the file or directory first, and then symlink the new config match target_path.is_dir() { true => fs_extra::dir::remove(target_path) .expect(&format!("Error: Unable to remove directory: {:?}", target_path)), @@ -44,69 +48,85 @@ pub fn install_configs(args: & cli::Cli) -> std::io::Result<()> { .expect(&format!("Error: Unable to remove file: {:?}", target_path)), }; // Try to symlink the config again, if failure exit with error - std::os::unix::fs::symlink(config_path, target_path) - .expect(&format!("Unable to symlink config: {:?}", config_path)); + std::os::unix::fs::symlink(config_path, target_path)?; }, } } + Ok(()) } +/// Handles collisions between existing files and dotfiles we're installing fn handle_collisions(args : & cli::Cli, - config_map : & fs::HashMap) -> io::Result<()> { - let conflicts = check_collisions(&config_map) - .expect("Error: Failed to check collisions"); + config_map : & fs::HashMap) -> Result<()> { + // Check if we found any collisions in the configurations + match check_collisions(&config_map) { + None => { + return Ok(()) // There were no collisions, configurations pass pre-install checks + }, + Some(conflicts) => { + // 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 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 + // TODO: Group this in with the --force flag?; Or make a new --adopt flag? + match io::prompt(msg) { + true => return Ok(()), + false => { + // Backup each conflicting config at the install location + for backup_target in conflicts.iter() { + backup_config(backup_target, &args)?; + } + }, + }; - // 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(); +fn backup_config(config_path: & fs::PathBuf, args: & cli::Cli) -> Result<()> { + let mut backup_path = args.backup_dir.to_owned(); backup_path.push(config_path.file_name().unwrap()); + + // Check if the configuration we're backing up is a directory or a single file match config_path.is_dir() { true => { - // Copy directory with recursion using fs_extra::dir::move_dir + // Copy directory with recursion using move_dir() wrapper function 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) + options.overwrite = args.force; + if let Err(e) = fs::move_dir(config_path, &backup_path, Some(&options)) + .map_err(|e| e.into()) { + return Err(e) + } }, 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) + options.overwrite = args.force; + if let Err(e) = fs::move_file(config_path, &backup_path, Some(&options)) + .map_err(|e| e.into()) { + return Err(e) + } }, - }.expect(&format!("Error: Unable to backup config: {:?}", config_path)); + } + Ok(()) +} + +// TODO: Function to uninstall configs. +// + Loops through dotfiles and restore backup files or delete configs +fn _uninstall_configs() -> Result<()> { Ok(()) } diff --git a/src/kot/cli.rs b/src/kot/cli.rs index 610bac1..539818f 100644 --- a/src/kot/cli.rs +++ b/src/kot/cli.rs @@ -6,6 +6,7 @@ ## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## ##############################################################################*/ +use std::path::Path; use structopt::StructOpt; // ============================================================================= @@ -14,6 +15,7 @@ use structopt::StructOpt; // ----------------------------------------------------------------------------- +/// Struct to outline behavior and features of kot CLI #[derive(Debug, StructOpt)] #[structopt( name="kot", @@ -44,6 +46,12 @@ pub struct Cli { parse(from_os_str) )] pub backup_dir: std::path::PathBuf, + + #[structopt( + help="Overwrites existing backups", + short, long + )] + pub force: bool, } // ============================================================================= @@ -52,28 +60,54 @@ pub struct Cli { // ----------------------------------------------------------------------------- -// Augment implementation of from_args to limit scope of StructOpt -// + Also enforces use of Cli::normalize() -// https://docs.rs/structopt/0.3.23/src/structopt/lib.rs.html#1121-1126 -pub fn from_args() -> Cli { +/// Augment implementation of from_args to limit scope of StructOpt +/// + Also enforces use of Cli::normalize() +/// + https://docs.rs/structopt/0.3.23/src/structopt/lib.rs.html#1121-1126 +pub fn from_args() -> super::Result { let s = Cli::from_clap(&Cli::clap().get_matches()); s.normalize() } impl Cli { - // Helper function to normalize arguments passed to program - pub fn normalize(mut self) -> Self { + /// Helper function to normalize arguments passed to program + pub fn normalize(mut self) -> super::Result { // If the path to the dotfiles doesn't exist, exit with error if !&self.dotfiles_dir.exists() { panic!("Error: Dotfiles configuration at {:?} does not exist", self.dotfiles_dir); } - self.dotfiles_dir = self.dotfiles_dir.canonicalize().unwrap(); + self.dotfiles_dir = self.dotfiles_dir.canonicalize()?; // If either the install or backup dir don't exist, create them - std::fs::create_dir_all(&self.install_dir).ok(); - self.install_dir = self.install_dir.canonicalize().unwrap(); - std::fs::create_dir_all(&self.backup_dir).ok(); - self.backup_dir = self.backup_dir.canonicalize().unwrap(); - self + std::fs::create_dir_all(&self.install_dir)?; + self.install_dir = self.install_dir.canonicalize()?; + std::fs::create_dir_all(&self.backup_dir)?; + self.backup_dir = self.backup_dir.canonicalize()?; + + // + To enforce the correction when error is encountered + // Get the number of configs currently in backup directory + // + An empty backup directory returns a count of 1 + let current_backups = self.backup_dir.read_dir()?.count(); + // If there are files in the backup directory already + if current_backups > 1 { + // If the --force flag is not set, warn and abort + if !self.force { + panic!("\n Error: Backups already exist at {:?}\ + \n Set the --force flag to overwrite configurations stored here" , self.backup_dir) + } + // If the --force flag is set, remove backups and create new + // + Move backups to /tmp/ + // + If we encounter an error, we can move these temp files back to args.backup_dir + // + On success we can delete them since new backups will have been created at args.backup_dir + let mut options = fs_extra::dir::CopyOptions::new(); + options.copy_inside = true; + options.overwrite = true; + let mut temp_path = Path::new("/tmp/").to_path_buf(); + temp_path.push(self.backup_dir.file_name().unwrap()); + // Move the old backups to /tmp/ and create a new empty backup directory + super::fs::move_dir( &self.backup_dir, &temp_path, Some(&options))?; + std::fs::create_dir_all(&self.backup_dir)?; + } + + Ok(self) } } diff --git a/src/kot/fs.rs b/src/kot/fs.rs index 9b222de..86b59b6 100644 --- a/src/kot/fs.rs +++ b/src/kot/fs.rs @@ -19,39 +19,36 @@ use std::fs; // ----------------------------------------------------------------------------- -// 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> { +/// 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::Result> { let mut config_map = HashMap::new(); // Local variable for the installation directory as an absolute path let mut config_target = args.install_dir.to_owned(); // For each file or directory within the dotfiles we're installing for config_entry in fs::read_dir(&args.dotfiles_dir)? { - // Match result from reading each item in dotfiles, return error if any - match config_entry { - Err(err) => return Err(err), - Ok(entry) => { - // Create full path to target config file (or directory) by push onto install path - config_target.push(entry.file_name()); + let entry = config_entry?; + // Create full path to target config file (or directory) by push onto install path + config_target.push(entry.file_name()); - // 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()); + // 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()); - // Reset config_target to be equal to requested install_dir - config_target.pop(); - }, - } + // Reset config_target to be equal to requested install_dir + config_target.pop(); } Ok(config_map) } -pub fn check_collisions(config_map : & HashMap) -> super::io::Result> { +/// Checks if any config to install collides with existing files or directories +/// + Returns a count of collisions within Some(), else returns None +pub fn check_collisions(config_map : & HashMap) -> Option> { let mut config_conflicts = vec![]; for (_path, target_config) in config_map.iter() { // If the target configuration file or directory already exists @@ -59,6 +56,35 @@ pub fn check_collisions(config_map : & HashMap) -> super::io:: config_conflicts.push(target_config.to_owned()); } } - Ok(config_conflicts) + if config_conflicts.len() > 0 { + return Some(config_conflicts) + } + return None } +/// Moves a single file from one location to another; Can be used to rename files +/// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided +pub fn move_file(src: & PathBuf, dst: & PathBuf, + options: Option< & fs_extra::file::CopyOptions>) -> super::Result<()> { + if options.is_none() { + // Default CopyOptions for moving files + let mut options = fs_extra::file::CopyOptions::new(); + options.overwrite = false; + } + fs_extra::file::move_file(src, dst, options.unwrap())?; + Ok(()) +} + +/// Moves a directory and all of it's contents recursively +/// + To specify options such as overwrite for the copy operation, a custom CopyOptions can be provided +pub fn move_dir(src: & PathBuf, dst: & PathBuf, + options: Option< & fs_extra::dir::CopyOptions>) -> super::Result<()> { + if options.is_none() { + // Default CopyOptions for moving directories + let mut options = fs_extra::dir::CopyOptions::new(); + options.copy_inside = true; + options.overwrite = false; + } + fs_extra::dir::move_dir(src, dst, options.unwrap())?; + Ok(()) +} diff --git a/src/kot/io.rs b/src/kot/io.rs index 52fbc12..e660508 100644 --- a/src/kot/io.rs +++ b/src/kot/io.rs @@ -17,8 +17,8 @@ use std::io; // ----------------------------------------------------------------------------- -// Asks user for y/n Y/N input, returns true/false respectively -// + Prompt output defined by msg parameter String +/// Asks user for y/n Y/N input, returns true/false respectively +/// + Prompt output defined by msg parameter String pub fn prompt(msg: String) -> bool { println!("{}", msg); let mut reply = String::new(); diff --git a/src/main.rs b/src/main.rs index 222d9d1..551bc82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,8 @@ ## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## ##############################################################################*/ +use std::path::PathBuf; + mod kot; // ============================================================================= @@ -14,9 +16,9 @@ mod kot; // ----------------------------------------------------------------------------- -fn main() { +fn main() -> kot::Result<()> { // Call augmented kot::cli::from_args() to parse CLI arguments - let args = kot::cli::from_args(); + let args = kot::cli::from_args()?; // At this point all paths exist and have been converted to absolute paths println!("args: {:?}\n", args); @@ -26,10 +28,19 @@ fn main() { // If there was an error, show the error type and run settings println!( "Error: {:?}\n+ Configs used: {:?}\n+ Install directory: {:?}\n", - e.kind(), args.dotfiles_dir, args.install_dir - ) + e, args.dotfiles_dir, args.install_dir + ); + + // If we were forcing a backup and met some error, revert backups to last good state + // TODO: Isolate this to limit error scope to backup related functions + if args.force { + let mut temp_path : PathBuf = kot::fs::Path::new("/tmp/").to_path_buf(); + temp_path.push(args.backup_dir.file_name().unwrap()); + kot::fs::move_dir(&temp_path, &args.backup_dir, None)?; + } }, - // Configurations installed successfully - Ok(()) => (), + _ => () } + // Configurations installed successfully + Ok(()) }