Handle input in environment

This commit is contained in:
Leonard Steppy 2025-02-04 23:09:02 +01:00
parent 85b0b3dbbf
commit 0632d7b0a9
2 changed files with 69 additions and 50 deletions

View File

@ -1,18 +1,19 @@
use std::env;
use std::env::VarError; use std::env::VarError;
use std::ffi::{OsStr, OsString}; use std::ffi::{OsStr, OsString};
use std::path::PathBuf; use std::path::PathBuf;
use std::{env, io};
pub trait Environment { pub trait Environment {
fn args_os(&self) -> Vec<OsString>; fn args_os(&self) -> Vec<OsString>;
fn var<K>(&self, key: K) -> Result<String, VarError> fn var<K>(&self, key: K) -> Result<String, VarError>
where where
K: AsRef<OsStr>; K: AsRef<OsStr>;
fn set_var<K, V>(&self, key: K, value: V) fn set_var<K, V>(&mut self, key: K, value: V)
where where
K: AsRef<OsStr>, K: AsRef<OsStr>,
V: AsRef<OsStr>; V: AsRef<OsStr>;
fn get_home_directory(&self) -> Option<PathBuf>; fn get_home_directory(&self) -> Option<PathBuf>;
fn read_line(&mut self) -> Result<String, io::Error>;
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -30,7 +31,7 @@ impl Environment for Prod {
env::var(key) env::var(key)
} }
fn set_var<K, V>(&self, key: K, value: V) fn set_var<K, V>(&mut self, key: K, value: V)
where where
K: AsRef<OsStr>, K: AsRef<OsStr>,
V: AsRef<OsStr> V: AsRef<OsStr>
@ -41,4 +42,10 @@ impl Environment for Prod {
fn get_home_directory(&self) -> Option<PathBuf> { fn get_home_directory(&self) -> Option<PathBuf> {
homedir::my_home().ok().flatten() homedir::my_home().ok().flatten()
} }
fn read_line(&mut self) -> Result<String, io::Error> {
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
Ok(buffer.trim().to_string())
}
} }

View File

@ -141,24 +141,6 @@ enum OldVersionPolicy {
Delete, Delete,
} }
//TODO IO would also need to be handled by the environment
#[macro_export]
macro_rules! input {
($prompt: tt) => {{
print!($prompt);
io::stdout().flush().expect("failed to flush stdout");
let mut buf = String::new();
io::stdin()
.read_line(&mut buf)
.expect("failed to read stdin");
buf.trim().to_string()
}};
() => {
input!()
};
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Application<E> { pub struct Application<E> {
pub environment: E, pub environment: E,
@ -168,12 +150,14 @@ impl<E> Application<E>
where where
E: Environment, E: Environment,
{ {
pub fn run(&self) -> Result<(), String> { pub fn run(&mut self) -> Result<(), String> {
let args = Args::try_parse_from(self.environment.args_os()).map_err(|e| e.to_string())?; let args = Args::try_parse_from(self.environment.args_os()).map_err(|e| e.to_string())?;
self.run_with_args(args) self.run_with_args(args)
} }
pub fn run_with_args(&self, args: Args) -> Result<(), String> { pub fn run_with_args(&mut self, args: Args) -> Result<(), String> {
let env = &mut self.environment;
let logger = Logger { let logger = Logger {
//all the below options are conflicting with each other so an if else is fine //all the below options are conflicting with each other so an if else is fine
level: if args.quiet { level: if args.quiet {
@ -216,7 +200,6 @@ where
} => { } => {
Self::require_non_empty_servers(&servers)?; Self::require_non_empty_servers(&servers)?;
Self::require_non_empty(&files, "files to upload")?; Self::require_non_empty(&files, "files to upload")?;
self.start_ssh_agent(&logger)?;
//resolve file server //resolve file server
let file_server = match file_server { let file_server = match file_server {
@ -230,6 +213,8 @@ where
None => None, None => None,
}; };
self.start_ssh_agent(&logger)?;
//make sure files exist //make sure files exist
match &file_server { match &file_server {
Some(file_server) => match &file_server.address { Some(file_server) => match &file_server.address {
@ -448,14 +433,9 @@ where
} }
} }
if !no_confirm { if !no_confirm && !self.confirm("Continue?", true) {
match input!("Continue? [Y|n] ").to_lowercase().as_str() { log!(logger, "Aborting...");
"n" | "no" => { return Ok(());
log!(logger, "Aborting...");
return Ok(());
}
_ => {}
}
} }
for server_actions in actions { for server_actions in actions {
@ -557,7 +537,8 @@ where
let download_directory = match download_directory { let download_directory = match download_directory {
Some(download_directory) => download_directory, Some(download_directory) => download_directory,
None => { None => {
let home_dir = self.get_home_directory() let home_dir = self
.get_home_directory()
.map_err(|e| format!("Missing download-directory: {e}"))?; .map_err(|e| format!("Missing download-directory: {e}"))?;
home_dir.join("Downloads") home_dir.join("Downloads")
} }
@ -588,14 +569,8 @@ where
download_directory.to_string_lossy() download_directory.to_string_lossy()
); );
if !args.quiet { if !args.quiet && self.confirm(format!("{duplication_notification}. Do you want to replace it?"), false) {
match input!("{duplication_notification}. Do you want to replace it? [N|y] ") break 'duplicate_check;
.to_lowercase()
.as_str()
{
"y" | "yes" => break 'duplicate_check,
_ => {}
}
} }
return Err(format!( return Err(format!(
@ -677,8 +652,8 @@ where
Ok(()) Ok(())
} }
fn start_ssh_agent(&self, logger: &Logger) -> Result<(), String> { fn start_ssh_agent(&mut self, logger: &Logger) -> Result<(), String> {
let env = &self.environment; let env = &mut self.environment;
//start the ssh agent //start the ssh agent
let agent_output = ShellCmd::new("ssh-agent") let agent_output = ShellCmd::new("ssh-agent")
@ -711,9 +686,36 @@ where
.map_err(|_| format!("Missing environment variable {}", SERVERS_ENV_VAR)) .map_err(|_| format!("Missing environment variable {}", SERVERS_ENV_VAR))
.and_then(|value| parse_server_configuration(&value, || self.get_home_directory())) .and_then(|value| parse_server_configuration(&value, || self.get_home_directory()))
} }
fn get_home_directory(&self) -> Result<PathBuf, String> { fn get_home_directory(&self) -> Result<PathBuf, String> {
self.environment.get_home_directory().ok_or("Failed to find your home directory".to_string()) self
.environment
.get_home_directory()
.ok_or("Failed to find your home directory".to_string())
}
fn confirm<S>(&mut self, prompt: S, default_value: bool) -> bool
where
S: ToString,
{
loop {
print!(
"{}[{}]",
prompt.to_string(),
if default_value { "Y|n" } else { "y|N" }
);
io::stdout().flush().expect("failed to flush stdout");
let line = self
.environment
.read_line()
.expect("Failed to read console input");
match line.to_lowercase().as_str() {
"" => return default_value,
"y" | "yes" => return true,
"n" | "no" => return false,
_ => println!("Invalid input, please choose one of the provided options"),
}
}
} }
} }
@ -736,12 +738,22 @@ fn osstring_from_ssh_output(output: Vec<u8>) -> OsString {
} }
} }
fn parse_server_configuration<F>(config_str: &str, get_home_directory: F) -> Result<Vec<Server>, String> where F: Fn() -> Result<PathBuf, String> { fn parse_server_configuration<F>(
config_str: &str,
get_home_directory: F,
) -> Result<Vec<Server>, String>
where
F: Fn() -> Result<PathBuf, String>,
{
config_str config_str
.split(',') .split(',')
.map(|server_entry| { .map(|server_entry| {
Server::from_str(server_entry, RelativeLocalPathAnker::Home, &get_home_directory) Server::from_str(
.map_err(|e| format!("Invalid server entry '{server_entry}': {e}")) server_entry,
RelativeLocalPathAnker::Home,
&get_home_directory,
)
.map_err(|e| format!("Invalid server entry '{server_entry}': {e}"))
}) })
.collect() .collect()
} }
@ -755,8 +767,8 @@ mod test {
#[test] #[test]
fn test_parse_server_configuration() { fn test_parse_server_configuration() {
let servers = let servers = parse_server_configuration("foo:bar,.:fizz/buzz", || Ok(PathBuf::from("/test")))
parse_server_configuration("foo:bar,.:fizz/buzz", || Ok(PathBuf::from("/test"))).expect("valid server configuration"); .expect("valid server configuration");
assert_eq!( assert_eq!(
vec![ vec![
Server { Server {