kot/src/kot/kcli.rs

194 lines
7.2 KiB
Rust

/*##############################################################################
## Author: Shaun Reed ##
## Legal: All Content (c) 2021 Shaun Reed, all rights reserved ##
## About: Wrapper for StructOpt crate functionality used by kot ##
## ##
## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ##
##############################################################################*/
use std::borrow::Borrow;
use std::path::{Path, PathBuf};
use regex::Regex;
use structopt::StructOpt;
use crate::kot::kerror::{Error, ErrorKind};
use crate::kot::err;
use crate::kot::kfs::create_dir_all;
use chrono;
use super::kfs;
// =============================================================================
// STRUCTS
// =============================================================================
// -----------------------------------------------------------------------------
/// CLI for managing Linux user configurations
#[derive(Debug, StructOpt)]
#[structopt(name = "kot")]
pub struct Cli {
/// Local or full path to user configurations to install. Can also be a git repository.
///
/// System path or repository URL for dotfiles we want to install.
/// If a path is used, it can either be local to CWD or absolute.
/// If a URL is used for a dotfiles repository, the repo is cloned into $HOME/.local/shared/kot/dotfiles/
#[structopt(parse(from_os_str))]
pub dotfiles: PathBuf,
/// The location to attempt installation of user configurations
///
/// The desired installation directory for user configurations.
/// By default this is your $HOME directory
/// This could optionally point to some other directory to perform a dry run, or the --dry-run flag could be set
#[structopt(
env = "HOME", // Default value to env variable $HOME
name = "install",
short, long,
parse(from_os_str)
)]
pub install_dir: PathBuf,
/// The location to store backups for this user
///
/// If no backup-dir is provided, we create one within the default kot data directory:
/// $HOME/.local/share/kot/backups/
#[structopt(
name = "backup-dir",
short, long,
parse(from_os_str)
)]
pub backup_dir: Option<PathBuf>,
/// An alternate path to clone a dotfiles repository to
///
/// If the clone-dir option is provided to the CLI, kot will clone the dotfiles repository into this directory.
/// If clone-dir is not provided, the repository is cloned into $HOME/.local/share/kot/dotfiles
/// Custom clone-dir will be used literally, and no subdirectory is created to store the cloned repository
/// For example, clone-dir of $HOME/clonedir for repo named Dotfiles
/// We will clone into $HOME/clonedir, and NOT $HOME/clonedir/Dotfiles
/// The default path for cloned repos is $HOME/.local/share/kot/dotfiles/
#[structopt(
name = "clone-dir",
short, long,
parse(from_os_str)
)]
pub clone_dir: Option<PathBuf>,
/// Overwrites existing backups
///
/// This flag will replace existing backups if during installation we encounter conflicts
/// and the backup-dir provided already contains previous backups.
#[structopt(
name = "force",
short, long
)]
pub force: bool,
/// Installs configurations to $HOME/.local/shared/kot/dry-runs
///
/// Useful flag to set when testing what an install would do to your home directory.
/// This is synonymous with setting --install $HOME/.local/shared/kot/dry-runs/$USER.
/// Subsequent runs with this flag set will not delete the contents of this directory.
#[structopt(
name = "dry-run",
short, long
)]
pub dry_run: bool,
// Indicates if dotfiles is a git repository URL; Not used by CLI directly
// + Initialized with result of regex pattern matching
#[structopt(skip)]
pub is_repo: bool,
// Not used by CLI, used to uninstall dotfiles when error is hit
#[structopt(skip)]
pub conflicts: Vec<PathBuf>,
}
// =============================================================================
// IMPLEMENTATION
// =============================================================================
// -----------------------------------------------------------------------------
/// 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<Cli> {
let s = Cli::from_clap(&Cli::clap().get_matches());
s.normalize()
}
impl Cli {
/// Helper function to normalize arguments passed to program
/// + Checks if dotfiles path is a repository URL
/// + If dotfiles path is not a repo URL, checks the path exists on the system
/// + Verifies install directory exists
/// + Verifies backup directory exists and does not already contain backups
pub fn normalize(mut self) -> super::Result<Self> {
// Determine if the dotfiles were provided as a github repository URL
let re_git = Regex::new(
r"^(([A-Za-z0-9]+@|http(|s)://)|(http(|s)://[A-Za-z0-9]+@))([A-Za-z0-9.]+(:\d+)?)(?::|/)([\d/\w.-]+?)(\.git){1}$"
);
self.is_repo = re_git.unwrap().is_match(&self.dotfiles.to_str().unwrap());
if self.is_repo {
// If the dotfiles were provided as a repository URL initialize clone_dir
self.clone_dir = match &self.clone_dir {
Some(d) => {
kfs::create_dir_all(d)?;
Some(kfs::abs(d)?)
},
None => Some(kfs::get_repo_path(self.dotfiles.to_str().unwrap()))
};
}
else {
// If the dotfiles were provided as a path, canonicalize it
self.dotfiles = kfs::abs(&self.dotfiles)?;
}
//
// If either the install, backup, or clone dir does not exist, create them
if self.dry_run {
self.install_dir = Path::new(
&(env!("HOME").to_owned() + &"/.local/share/kot/dry-runs/" + env!("USER"))
).to_path_buf();
}
self.install_dir = kfs::create_dir_all(&self.install_dir)?;
// If the CLI was not provided a backup_dir, use default naming convention
match self.backup_dir {
None => {
let mut backup_dir = kfs::get_data_dir();
backup_dir.push("backups/");
backup_dir.push(self.dotfiles.file_name().unwrap().to_str().unwrap().to_owned()
+ ":" + &*chrono::offset::Local::now()
.format("%Y-%m-%dT%H:%M:%S").to_string()
);
self.backup_dir = Some(kfs::create_dir_all(&backup_dir)?);
}
Some(dir) => {
// If a backup_dir was given to CLI, use it instead of default
self.backup_dir = Some(kfs::create_dir_all(&dir)?);
}
}
//
// Check if the backup directory provided is empty
// If there are files and the --force flag is not set, warn and abort
if !self.force && kfs::dir_entries(&self.backup_dir.as_ref().unwrap())? > 1 {
return err!(
ErrorKind::ConfigError(format!("Backups already exist at: {:?}", &self.backup_dir)),
"Set the --force flag to overwrite configurations stored here".to_owned()
);
}
// If the --force flag is set, stash backup files in /tmp/ and create new
kfs::stash_dir(&self.backup_dir.as_ref().unwrap())?;
// Available CLI options pass initial checks; Return them to caller
return Ok(self);
}
}