diff --git a/src/command.rs b/src/command.rs index 2dfd484..8d76811 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,3 +1,5 @@ +#[deprecated] + use crate::log; use crate::logger::{LogLevel, Logger}; use crate::shell_interface::command_to_string; diff --git a/src/main.rs b/src/main.rs index ba9d91d..199d4c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,14 +9,13 @@ mod server; mod shell_interface; use crate::action::{Action, FileAction, ServerActions}; -use crate::command::LogRunnable; use crate::environment::{Environment, Prod}; use crate::file::{FileMatcher, FileNameInfo}; use crate::logger::{LogLevel, Logger}; use crate::os_str_extension::OsStrExtension; use crate::os_string_builder::ReplaceWithOsStr; use crate::server::{RelativeLocalPathAnker, ServerAddress}; -use crate::shell_interface::os_string_from_ssh_output; +use crate::shell_interface::{ScpParam, ServerCommand, ShellCommand, ShellInterface}; use clap::{Parser, Subcommand, ValueEnum}; use lazy_regex::{lazy_regex, Lazy, Regex}; use server::{Server, ServerReference}; @@ -32,8 +31,6 @@ const SERVERS_ENV_VAR: &str = "MSSH_SERVERS"; const EDITOR_ENV_VAR: &str = "MSSH_EDITOR"; const FILE_PLACEHOLDER: &str = ""; -type ShellCmd = std::process::Command; - /// Uploads a file or executes a command on multiple configured servers /// /// Servers must either be configured via environment variable or denote their server directory with @@ -155,7 +152,11 @@ where } pub fn run_with_args(&mut self, args: Args) -> Result<(), String> { - let _env = &mut self.environment; + macro_rules! env { + () => { + &mut self.environment + }; + } let logger = Logger { //all the below options are conflicting with each other so an if else is fine @@ -231,23 +232,29 @@ where files = files .iter() .map(|file| { - let output = ShellCmd::new("ssh") - .arg(ssh_address) - .arg(osf!("realpath -e ") + file_server.server_directory_path.join(file)) - .collect_full_output() - .map_err(|e| format!("Failed to canonicalize files: {e}"))?; + let output = ShellCommand::Ssh { + address: ssh_address.to_string(), + server_command: ServerCommand::Realpath { + path: file_server.server_directory_path.join(file), + }, + } + .in_env(env!()) + .output() + .into_result() + .map_err(|e| format!("Failed to canonicalize files: {e}"))?; - if !output.status.success() { + if !output.status.success { Err(format!( "Path doesn't match any files on file-server: {}", file.to_string_lossy() ))?; } - let denoted_files = os_string_from_ssh_output(output.stdout) + let denoted_files = output + .stdout .split(b'\n') //split at line breaks .into_iter() - .filter(|bytes| !bytes.is_empty()) //needed since realpath sometimes gives us empty lines + .filter(|file_name| !file_name.is_empty()) //needed since realpath sometimes gives us empty lines .map(PathBuf::from) .collect::>(); @@ -305,19 +312,23 @@ where server, actions: { let present_file_names: Vec = match &server.address { - ServerAddress::Ssh { ssh_address } => os_string_from_ssh_output( - ShellCmd::new("ssh") - .arg(ssh_address) - .arg(osf!("ls ") + &working_directory) - .collect_output() - .map_err(|e| { - format!( - "Failed to query present files on server {}: {e}", - server.get_name() - ) - })? - .stdout, - ) + ServerAddress::Ssh { ssh_address } => ShellCommand::Ssh { + address: ssh_address.to_string(), + server_command: ServerCommand::Ls { + dir: working_directory.clone(), + }, + } + .in_env(env!()) + .output() + .and_expect_success() + .into_result_with_error_logging(&logger) + .map_err(|e| { + format!( + "Failed to query present files on server {}: {e}", + server.get_name() + ) + })? + .stdout .split(b'\n') .into_iter() .map(OsString::from) @@ -452,32 +463,37 @@ where for file_action in server_actions.actions { match file_action.kind { Action::Add | Action::Replace => { - let scp_source = match &file_server { - Some(file_server) => { - osf!(match &file_server.address { - ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), - ServerAddress::Localhost => "".to_string(), - }) + file_server.server_directory_path.join(&file_action.file) - } - None => osf!(&file_action.file), + let source = match &file_server { + Some(file_server) => ScpParam::from(( + file_server, + file_server.server_directory_path.join(&file_action.file), + )), + None => ScpParam::from(file_action.file.as_path()), }; - let scp_target = osf!(match &server.address { - ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), - ServerAddress::Localhost => "".to_string(), - }) + &server_actions.working_directory; - ShellCmd::new("scp") - .arg(scp_source) - .arg(scp_target) - .run(&logger) - .map_err(|e| format!("upload failure: {e}"))?; + let destination = ScpParam::from((server, &server_actions.working_directory)); + ShellCommand::Scp { + source, + destination, + } + .in_env(env!()) + .run_logged(&logger) + .and_expect_success() + .into_result_with_error_logging(&logger) + .map_err(|e| format!("upload failure: {e}"))?; } Action::Delete => match &server.address { ServerAddress::Ssh { ssh_address } => { - ShellCmd::new("ssh") - .arg(ssh_address) - .arg(osf!("rm ") + server_actions.working_directory.join(&file_action.file)) - .run(&logger) - .map_err(|e| format!("failed to delete old version: {e}"))?; + ShellCommand::Ssh { + address: ssh_address.to_string(), + server_command: ServerCommand::Rm { + file: server_actions.working_directory.join(&file_action.file), + }, + } + .in_env(env!()) + .run_logged(&logger) + .and_expect_success() + .into_result_with_error_logging(&logger) + .map_err(|e| format!("failed to delete old version: {e}"))?; } ServerAddress::Localhost => { fs::remove_file(server_actions.working_directory.join(&file_action.file)) @@ -486,16 +502,18 @@ where }, Action::Rename { new_name } => match &server.address { ServerAddress::Ssh { ssh_address } => { - ShellCmd::new("ssh") - .arg(ssh_address) - .arg( - osf!("mv ") - + server_actions.working_directory.join(&file_action.file) - + " " - + server_actions.working_directory.join(&new_name), - ) - .run(&logger) - .map_err(|e| format!("failed to rename: {e}"))?; + ShellCommand::Ssh { + address: ssh_address.to_string(), + server_command: ServerCommand::Mv { + source: server_actions.working_directory.join(&file_action.file), + destination: server_actions.working_directory.join(&new_name), + }, + } + .in_env(env!()) + .run_logged(&logger) + .and_expect_success() + .into_result_with_error_logging(&logger) + .map_err(|e| format!("failed to rename: {e}"))?; } ServerAddress::Localhost => { let dir = &server_actions.working_directory; @@ -516,20 +534,33 @@ where log!(logger, "Running command on '{}'...", server.get_name()); match &server.address { ServerAddress::Ssh { ssh_address } => { - ShellCmd::new("ssh") - .arg(ssh_address) - .arg(osf!("cd ") + server.server_directory_path + "; " + &command) - .run(&logger) - .map_err(|e| format!("{e}"))?; + ShellCommand::Ssh { + address: ssh_address.to_string(), + server_command: ServerCommand::Execute { + working_directory: server.server_directory_path.clone(), + command: OsString::from(&command), + }, + } + .in_env(env!()) + .run_logged(&logger) + .and_expect_success() + .into_result_with_error_logging(&logger) + .map_err(|e| format!("{e}"))?; } ServerAddress::Localhost => { - let mut command_args = shell_words::split(&command) - .map_err(|e| format!("failed to parse command: {e}"))?; - ShellCmd::new(command_args.remove(0)) - .args(&command_args) - .current_dir(&server.server_directory_path) - .run(&logger) - .map_err(|e| format!("{e}"))?; + let command = shell_words::split(&command) + .map_err(|e| format!("failed to parse command: {e}"))? + .into_iter() + .map(OsString::from) + .collect(); + ShellCommand::Execute { + working_directory: server.server_directory_path.clone(), + command, + } + .in_env(env!()) + .run_logged(&logger) + .and_expect_success() + .into_result_with_error_logging(&logger)?; } } } @@ -597,18 +628,19 @@ where for server in servers { log!(logger, "Getting file from {}...", server.get_name()); - let file_source = osf!(match &server.address { - ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), - ServerAddress::Localhost => "".to_string(), - }) + server.server_directory_path.join(&file); - ShellCmd::new("scp") - .arg(&file_source) - .arg(&download_directory) - .run(&logger) - .map_err(|e| format!("download failure: {e}"))?; + let source = ScpParam::from((&server, server.server_directory_path.join(&file))); + ShellCommand::Scp { + source: source.clone(), + destination: ScpParam::from(download_directory.as_path()), + } + .in_env(env!()) + .run_logged(&logger) + .and_expect_success() + .into_result_with_error_logging(&logger) + .map_err(|e| format!("download failure: {e}"))?; //open file in editor - let mut editor_command_args = shell_words::split(&editor) + let editor_command = shell_words::split(&editor) .map_err(|e| format!("failed to parse editor command: {e}"))? .into_iter() .map(|part| { @@ -616,18 +648,23 @@ where }) .collect::>(); - let command = editor_command_args.remove(0); - ShellCmd::new(command) - .args(editor_command_args) - .run(&logger) + ShellCommand::Editor(editor_command) + .in_env(env!()) + .run_logged(&logger) + .and_expect_success() + .into_result_with_error_logging(&logger) .map_err(|e| format!("failed to open file in editor: {e}"))?; //upload file again - ShellCmd::new("scp") - .arg(download_directory.join(file_name)) - .arg(&file_source) - .run(&logger) - .map_err(|e| format!("failed to re-upload file: {e}"))?; + ShellCommand::Scp { + source: ScpParam::from(download_directory.join(file_name).as_path()), + destination: source, + } + .in_env(env!()) + .run_logged(&logger) + .and_expect_success() + .into_result_with_error_logging(&logger) + .map_err(|e| format!("failed to re-upload file: {e}"))?; } log!(logger, "Done!"); @@ -669,25 +706,30 @@ where let env = &mut self.environment; //start the ssh agent - let agent_output = ShellCmd::new("ssh-agent") - .arg("-s") - .collect_output() - .map_err(|e| format!("failed to start ssh agent: {e}"))?; - let agent_stdout = String::from_utf8_lossy(&agent_output.stdout); - if !agent_output.status.success() { - return Err("failed to start ssh agent; maybe try to run ssh-agent manually?".to_string()); - } + let agent_output = ShellCommand::SshAgent + .in_env(env) + .output() + .and_expect_success() + .into_result_with_error_logging(logger) + .map_err(|e| format!("Failed to start ssh agent: {e}"))?; + let agent_stdout = &agent_output + .stdout + .into_string() + .map_err(|_| "ssh-agent returned invalid utf-8 - how did this even happen?")?; //set the env vars from the agent static ENV_VAR_REGEX: Lazy = lazy_regex!("(.+?)=(.+?);"); - for capture in ENV_VAR_REGEX.captures_iter(&agent_stdout) { + for capture in ENV_VAR_REGEX.captures_iter(agent_stdout) { let (_, [env_var, value]) = capture.extract(); env.set_var(env_var, value); } //add the ssh key - ShellCmd::new("ssh-add") - .run(logger) + ShellCommand::ShhAdd + .in_env(env) + .run_logged(logger) + .and_expect_success() + .into_result_with_error_logging(logger) .map_err(|e| format!("failed to add ssh-key: {e}"))?; Ok(()) } diff --git a/src/shell_interface.rs b/src/shell_interface.rs index a9fb1a1..75f0063 100644 --- a/src/shell_interface.rs +++ b/src/shell_interface.rs @@ -1,16 +1,20 @@ use crate::logger::{LogLevel, Logger}; +use crate::server::{Server, ServerAddress}; use crate::{log, osf}; use std::error::Error; use std::ffi::OsString; use std::fmt::{Debug, Display, Formatter}; use std::iter::once; -use std::path::PathBuf; +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use std::{io, process}; #[derive(Debug)] pub struct EnvCommand<'a, E> { command: ShellCommand, + phantom_data: PhantomData<&'a E>, + #[cfg(test)] environment: &'a mut E, } @@ -34,10 +38,12 @@ pub enum ShellCommand { } impl ShellCommand { - pub fn in_env(self, environment: &mut E) -> EnvCommand { + pub fn in_env(self, _environment: &mut E) -> EnvCommand { EnvCommand { command: self, - environment, + phantom_data: Default::default(), + #[cfg(test)] + environment: _environment, } } } @@ -69,6 +75,30 @@ pub struct ScpParam { pub path: PathBuf, } +impl

From<(&Server, P)> for ScpParam +where + P: AsRef, +{ + fn from((server, path): (&Server, P)) -> Self { + Self { + server: match &server.address { + ServerAddress::Ssh { ssh_address } => Some(ssh_address.into()), + ServerAddress::Localhost => None, + }, + path: PathBuf::from(path.as_ref()), + } + } +} + +impl From<&Path> for ScpParam { + fn from(value: &Path) -> Self { + Self { + server: None, + path: PathBuf::from(value), + } + } +} + impl From<&ScpParam> for OsString { fn from(value: &ScpParam) -> Self { let mut builder = osf!(); @@ -327,8 +357,7 @@ impl From for CommandOutput { } } -//TODO remove super visibility once it is not needed anymore -pub(super) fn os_string_from_ssh_output(output: Vec) -> OsString { +pub fn os_string_from_ssh_output(output: Vec) -> OsString { #[cfg(unix)] { use std::os::unix::ffi::OsStringExt; @@ -352,6 +381,7 @@ impl AsRef for CommandOutput { pub struct ExitStatus { pub success: bool, pub string_form: String, + #[allow(dead_code)] pub code: Option, } @@ -383,6 +413,15 @@ pub struct CommandError { pub error: E, } +impl From> for String +where + E: Display, +{ + fn from(value: CommandError) -> Self { + value.to_string() + } +} + impl Display for CommandError where E: Display,