diff --git a/README.md b/README.md index 2d250bc..4258902 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ #### kot -Learning to program in Rust by making myself a Linux CLI tool to help manage dotfiles and configurations. - +Learning to program in Rust by making myself a Linux CLI tool to help manage dotfiles and configurations. There are many other tools to manage dotfiles that work just fine. For now, this is intended to be just for my own learning / use and not a general dotfiles management utility. +Follow [Rustup instructions](https://rustup.rs/) to setup the Rust toolchain + +Then to build and run `kot`, run the following commands + ```bash -[kapper@kubuntu ~]$./kot --help +git clone https://gitlab.com/shaunrd0/kot && cd kot +cargo build +./target/debug/kot --help + kot 0.1.0 CLI utility for managing Linux user configurations @@ -27,22 +33,41 @@ ARGS: To store dotfiles, this repository uses submodules. To update surface-level submodules, we can run the following commands ```bash -git submodule init -git submodule update +git submodule update --init Submodule path 'dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843' ``` But in the case of my dotfiles repository, [shaunrd0/dot](https://gitlab.com/shaunrd0/dot), I use submodules to clone repositories for vim plugins. To update all submodules *and their nested submodules*, we can run the following commands ```bash -git submodule init -git submodule update --recursive -Submodule path 'dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843' -Submodule path 'dot/.vim/bundle/Colorizer': checked out '826d5691ac7d36589591314621047b1b9d89ed34' -Submodule path 'dot/.vim/bundle/ale': checked out '3ea887d2f4d43dd55d81213517344226f6399ed6' -Submodule path 'dot/.vim/bundle/clang_complete': checked out '293a1062274a06be61797612034bd8d87851406e' -Submodule path 'dot/.vim/bundle/supertab': checked out 'd80e8e2c1fa08607fa34c0ca5f1b66d8a906c5ef' -Submodule path 'dot/.vim/bundle/unicode.vim': checked out 'afb8db4f81580771c39967e89bc5772e72b9018e' -Submodule path 'dot/.vim/bundle/vim-airline': checked out 'cb1bc19064d3762e4e08103afb37a246b797d902' -Submodule path 'dot/.vim/bundle/vim-airline-themes': checked out 'd148d42d9caf331ff08b6cae683d5b210003cde7' -Submodule path 'dot/.vim/bundle/vim-signify': checked out 'b2a0450e23c63b75bbeabf4f0c28f9b4b2480689' +git submodule update --init --recursive + +Submodule 'dotfiles/dot' (https://gitlab.com/shaunrd0/dot) registered for path 'dotfiles/dot' +Cloning into '/home/kapper/Code/kotd/dotfiles/dot'... +warning: redirecting to https://gitlab.com/shaunrd0/dot.git/ +Submodule path 'dotfiles/dot': checked out '7877117d5bd413ecf35c86efb4514742d8136843' +Submodule '.vim/bundle/Colorizer' (https://github.com/chrisbra/Colorizer) registered for path 'dotfiles/dot/.vim/bundle/Colorizer' +Submodule '.vim/bundle/ale' (https://github.com/dense-analysis/ale) registered for path 'dotfiles/dot/.vim/bundle/ale' +Submodule '.vim/bundle/clang_complete' (https://github.com/xavierd/clang_complete) registered for path 'dotfiles/dot/.vim/bundle/clang_complete' +Submodule '.vim/bundle/supertab' (https://github.com/ervandew/supertab) registered for path 'dotfiles/dot/.vim/bundle/supertab' +Submodule '.vim/bundle/unicode.vim' (https://github.com/chrisbra/unicode.vim) registered for path 'dotfiles/dot/.vim/bundle/unicode.vim' +Submodule '.vim/bundle/vim-airline' (https://github.com/vim-airline/vim-airline) registered for path 'dotfiles/dot/.vim/bundle/vim-airline' +Submodule '.vim/bundle/vim-airline-themes' (https://github.com/vim-airline/vim-airline-themes) registered for path 'dotfiles/dot/.vim/bundle/vim-airline-themes' +Submodule '.vim/bundle/vim-signify' (https://github.com/mhinz/vim-signify) registered for path 'dotfiles/dot/.vim/bundle/vim-signify' +Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/Colorizer'... +Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/ale'... +Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/clang_complete'... +Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/supertab'... +Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/unicode.vim'... +Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-airline'... +Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-airline-themes'... +Cloning into '/home/kapper/Code/kotd/dotfiles/dot/.vim/bundle/vim-signify'... +Submodule path 'dotfiles/dot/.vim/bundle/Colorizer': checked out '826d5691ac7d36589591314621047b1b9d89ed34' +Submodule path 'dotfiles/dot/.vim/bundle/ale': checked out '3ea887d2f4d43dd55d81213517344226f6399ed6' +Submodule path 'dotfiles/dot/.vim/bundle/clang_complete': checked out '293a1062274a06be61797612034bd8d87851406e' +Submodule path 'dotfiles/dot/.vim/bundle/supertab': checked out 'd80e8e2c1fa08607fa34c0ca5f1b66d8a906c5ef' +Submodule path 'dotfiles/dot/.vim/bundle/unicode.vim': checked out 'afb8db4f81580771c39967e89bc5772e72b9018e' +Submodule path 'dotfiles/dot/.vim/bundle/vim-airline': checked out 'cb1bc19064d3762e4e08103afb37a246b797d902' +Submodule path 'dotfiles/dot/.vim/bundle/vim-airline-themes': checked out 'd148d42d9caf331ff08b6cae683d5b210003cde7' +Submodule path 'dotfiles/dot/.vim/bundle/vim-signify': checked out 'b2a0450e23c63b75bbeabf4f0c28f9b4b2480689' ``` + diff --git a/backups/kapper/README.md b/backups/kapper/README.md deleted file mode 100644 index 463d185..0000000 --- a/backups/kapper/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory is for testing the backup process of user configurations that conflict with configurations we're attempting to install. diff --git a/src/kot.rs b/src/kot.rs index bf1d93f..c5a8f81 100644 --- a/src/kot.rs +++ b/src/kot.rs @@ -16,6 +16,8 @@ pub mod io; // ----------------------------------------------------------------------------- +// Creates symbolic links to the configurations we're installing +// TODO: On error, revert to last good state 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 @@ -26,12 +28,16 @@ pub fn install_configs(args: & cli::Cli) -> std::io::Result<()> { println!("Installing config: {:?}\n+ At location: {:?}\n", config_path, target_path); match std::os::unix::fs::symlink(config_path, target_path) { - Ok(()) => (), + Ok(()) => (), // Configuration installed successfully Err(_e) => { + // Attempt to remove the file or directory, and then symlink the new config match target_path.is_dir() { - true => fs_extra::dir::remove(target_path), - false => fs_extra::file::remove(target_path), + true => fs_extra::dir::remove(target_path) + .expect(&format!("Error: Unable to remove directory: {:?}", target_path)), + false => fs_extra::file::remove(target_path) + .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)); }, diff --git a/src/kot/cli.rs b/src/kot/cli.rs index 5ef9d58..610bac1 100644 --- a/src/kot/cli.rs +++ b/src/kot/cli.rs @@ -24,13 +24,14 @@ pub struct Cli { help="Local or full path to user configurations to install", parse(from_os_str) )] - pub configs_dir: std::path::PathBuf, + pub dotfiles_dir: std::path::PathBuf, #[structopt( help="The location to attempt installation of user configurations", default_value="dry-runs/kapper", // TODO: Remove temp default value after tests // env = "HOME", // Default value to env variable $HOME - long="home-dir", + name="install-dir", + short, long, parse(from_os_str) )] pub install_dir: std::path::PathBuf, @@ -38,7 +39,8 @@ pub struct Cli { #[structopt( help="The location to store backups for this user", default_value="backups/kapper", - long="backup-dir", + name="backup-dir", + short, long, parse(from_os_str) )] pub backup_dir: std::path::PathBuf, @@ -61,8 +63,16 @@ pub fn from_args() -> Cli { impl Cli { // Helper function to normalize arguments passed to program pub fn normalize(mut self) -> Self { - self.configs_dir = self.configs_dir.canonicalize().unwrap(); + // 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(); + + // 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 } diff --git a/src/kot/fs.rs b/src/kot/fs.rs index 8d9f91f..bd9ea3d 100644 --- a/src/kot/fs.rs +++ b/src/kot/fs.rs @@ -19,20 +19,27 @@ 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(()) } @@ -41,27 +48,41 @@ fn backup_config(config_path: & PathBuf, backup_dir: & PathBuf) -> super::io::Re pub fn get_target_paths(args: & super::cli::Cli) -> super::io::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 config_entry in fs::read_dir(&args.configs_dir)? { + // 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()); + // If the target configuration file or directory already exists if config_target.exists() { - match super::io::prompt(format!("Configuration already exists: {:?}\nAbort? Enter y/n or Y/N: ", config_target)) { - true => return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists)),//panic!("User abort"), - false => backup_config(&config_target, &args.backup_dir).ok(), // TODO: Backup colliding configs + // Ask client if they would like to abort given the config collision + let msg = format!("Configuration already exists: {:?}\ + \nAbort? Enter y/n or Y/N: ", config_target); + + // If we abort, exit; If we continue, back up the configs + match super::io::prompt(msg) { + true => 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 + // 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(); }, } } + Ok(config_map) } diff --git a/src/kot/io.rs b/src/kot/io.rs index b5650a8..52fbc12 100644 --- a/src/kot/io.rs +++ b/src/kot/io.rs @@ -17,6 +17,8 @@ use std::io; // ----------------------------------------------------------------------------- +// 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(); @@ -25,6 +27,7 @@ pub fn prompt(msg: String) -> bool { match reply.trim() { "y" | "Y" => true, "n" | "N" => false, + // Handle garbage input _ => prompt("Please enter y/n or Y/N\n".to_owned()), } } diff --git a/src/main.rs b/src/main.rs index 6e2c002..222d9d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,12 +15,21 @@ mod kot; // ----------------------------------------------------------------------------- fn main() { + // Call augmented kot::cli::from_args() to parse CLI arguments let args = kot::cli::from_args(); + // At this point all paths exist and have been converted to absolute paths println!("args: {:?}\n", args); + // Attempt to install the configurations, checking for collisions match kot::install_configs(&args) { - Err(e) => println!("Error: {:?}\n+ Configs used: {:?}\n+ Install directory: {:?}\n", - e.kind(), args.configs_dir, args.install_dir), + Err(e) => { + // 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 + ) + }, + // Configurations installed successfully Ok(()) => (), } }