diff --git a/README.md b/README.md index f66b324..2cded86 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ Upload a new version of the MineZ plugin from the MineZ-Dev and keep backups of multi-ssh minez -u -S minez-dev plugins/MineZ-3.0.jar -a ``` +Upload of multiple files is also supported. + The program will show you an overview of what it will be doing before actually performing any of the actions: ``` minez (minez3/plugins): diff --git a/src/action.rs b/src/action.rs index c589629..c493fd0 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,7 +1,7 @@ use crate::server::Server; -use std::ffi::OsString; +use std::ffi::{OsStr, OsString}; use std::fmt::{Display, Formatter}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Debug)] pub struct ServerActions<'a> { @@ -12,7 +12,12 @@ pub struct ServerActions<'a> { impl Display for ServerActions<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}: ({})", self.server.get_name(), self.working_directory.to_string_lossy())?; + write!( + f, + "{}: ({})", + self.server.get_name(), + self.working_directory.to_string_lossy() + )?; for action in &self.actions { write!(f, "\n{}", action)?; } @@ -23,19 +28,35 @@ impl Display for ServerActions<'_> { #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct FileAction { pub file: PathBuf, + pub file_name: OsString, pub kind: Action, } +impl FileAction { + pub fn new

(file: P, kind: Action) -> Option + where + P: AsRef, + { + let file = PathBuf::from(file.as_ref()); + let file_name = file.file_name()?.to_os_string(); + Some(Self { + file, + file_name, + kind, + }) + } +} + impl Display for FileAction { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self.kind { - Action::Add => write!(f, "+ adding {}", self.file.to_string_lossy()), - Action::Replace => write!(f, "~ replacing {}", self.file.to_string_lossy()), - Action::Delete => write!(f, "- deleting {}", self.file.to_string_lossy()), + Action::Add => write!(f, "+ adding {}", self.file_name.to_string_lossy()), + Action::Replace => write!(f, "~ replacing {}", self.file_name.to_string_lossy()), + Action::Delete => write!(f, "- deleting {}", self.file_name.to_string_lossy()), Action::Rename { new_name } => write!( f, "* renaming {} -> {}", - self.file.to_string_lossy(), + self.file_name.to_string_lossy(), new_name.to_string_lossy() ), } @@ -49,3 +70,9 @@ pub enum Action { Delete, Rename { new_name: OsString }, } + +impl Action { + pub fn rename(new_name: S) -> Self where S: AsRef { + Self::Rename {new_name: new_name.as_ref().to_owned()} + } +} diff --git a/src/main.rs b/src/main.rs index 794450f..e9831ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ mod os_string_builder; mod server; use crate::action::{Action, FileAction, ServerActions}; -use crate::command::{ExecutionError, LogRunnable}; +use crate::command::{ExecutionError, LogRunnable, SpecificExecutionError}; use crate::file::{FileMatcher, FileNameInfo}; use crate::logger::{LogLevel, Logger}; use crate::os_string_builder::ReplaceWithOsStr; @@ -61,8 +61,8 @@ enum Command { /// Upload a file to the servers #[command(visible_short_flag_alias = 'u')] Upload { - /// The file to upload - file: PathBuf, + /// The files to upload + files: Vec, /// The ssh server to get the file from. /// /// When this option is set, the file path must be absolute, or relative to the server directory. @@ -178,7 +178,7 @@ fn main() -> Result<(), String> { match args.command { Command::Upload { - file, + files, file_server, old_version_policy, upload_directory, @@ -200,45 +200,35 @@ fn main() -> Result<(), String> { None => None, }; - //make sure file exists and is a file + //make sure files exist match &file_server { - Some(file_server) => { - match &file_server.address { - ServerAddress::Ssh { ssh_address } => { - match ShellCmd::new("ssh") - .arg(ssh_address) - .arg(osf!("test -f ") + file_server.server_directory_path.join(&file)) - .collect_output() - { - Ok(_) => {} //file exists on file server - Err(e) => { - match &e.error { - ExecutionError::StartError(_) => { - //error occurred - Err(format!( - "Failed to check whether file exists on file-server: {e}" - ))?; - } - ExecutionError::BadExitStatus(_) => { - //file does not exist on file server - Err("File doesn't exist on file server")?; - } - } - } - }; - } - ServerAddress::Localhost => { - check_local_file_exists(file_server.server_directory_path.join(&file))?; + Some(file_server) => match &file_server.address { + ServerAddress::Ssh { ssh_address } => { + for file in &files { + check_file_exists_on_server(file, ssh_address, &file_server.server_directory_path)?; } } - } + ServerAddress::Localhost => { + for file in &files { + check_local_file_exists(file_server.server_directory_path.join(file))?; + } + } + }, None => { - check_local_file_exists(&file)?; + for file in &files { + check_local_file_exists(file)?; + } } } - let file_name_info = - FileNameInfo::try_from(file.clone()).map_err(|e| format!("bad file: {e}"))?; + let file_details = files + .iter() + .map(|file| { + FileNameInfo::try_from(file.clone()) + .map(|info| (PathBuf::from(file), info)) + .map_err(|e| format!("Bad file '{}': {e}", file.to_string_lossy())) + }) + .collect::, _>>()?; //create overview of what has to be done on each server let actions = servers @@ -263,66 +253,61 @@ fn main() -> Result<(), String> { let ls_output = ls_command .collect_output() .map_err(|e| format!("failed to query files: {e}"))?; - let output = String::from_utf8_lossy(&ls_output.stdout); + let ls_output = String::from_utf8_lossy(&ls_output.stdout); - let mut file_matcher = - FileMatcher::from(file_name.as_ref().unwrap_or(&file_name_info.name)); - if let Some(extension) = file_name_info.extension.as_ref() { - file_matcher = file_matcher.and_extension(extension); - } - - let file_name = file_name_info.to_full_file_name(); - - let add_action = FileAction { - file: PathBuf::from(&file_name), - kind: Action::Add, - }; - let mut files = output.lines(); - match old_version_policy { - OldVersionPolicy::Ignore => { - vec![if files.any(|file| file == file_name) { - FileAction { - file: PathBuf::from(&file_name), - kind: Action::Replace, - } - } else { - add_action - }] - } - OldVersionPolicy::Archive => files - .filter(|file| file_matcher.matches(file)) - .map(|file| FileAction { - file: PathBuf::from(file), - kind: Action::Rename { - new_name: format!("{file}{}", file.chars().last().unwrap_or('1')).into(), - }, - }) - .chain(once(add_action)) - .collect(), - OldVersionPolicy::Delete => { - let mut actions: Vec<_> = files - .filter(|file| file_matcher.matches(file)) - .map(|file| { - //special case -> file has the same name as current file, then we just need to replace it - if file == file_name { - FileAction { - file: PathBuf::from(file), - kind: Action::Replace, - } - } else { - FileAction { - file: PathBuf::from(file), - kind: Action::Delete, - } - } - }) - .collect(); - if !actions.iter().any(|action| action.kind == Action::Replace) { - actions.push(add_action); + file_details + .iter() + .flat_map(|(file, file_name_info)| { + let mut file_matcher = + FileMatcher::from(file_name.as_ref().unwrap_or(&file_name_info.name)); + if let Some(extension) = file_name_info.extension.as_ref() { + file_matcher = file_matcher.and_extension(extension); } - actions - } - } + + let file_name = file_name_info.to_full_file_name(); + + let add_action = FileAction::new(file, Action::Add).expect("path points to file"); + + let mut ls_lines = ls_output.lines(); + match old_version_policy { + OldVersionPolicy::Ignore => { + vec![if ls_lines.any(|file| file == file_name) { + FileAction::new(&file_name, Action::Replace).expect("path points to file") + } else { + add_action + }] + } + OldVersionPolicy::Archive => ls_lines + .filter(|file| file_matcher.matches(file)) + .map(|file| { + FileAction::new( + file, + Action::rename(format!("{file}{}", file.chars().last().unwrap_or('1'))), + ) + .expect("path points to file") + }) + .chain(once(add_action)) + .collect(), + OldVersionPolicy::Delete => { + let mut actions = ls_lines + .filter(|file| file_matcher.matches(file)) + .map(|file| { + //special case -> file has the same name as current file, then we just need to replace it + if file == file_name { + FileAction::new(file, Action::Replace).expect("path points to file") + } else { + FileAction::new(file, Action::Delete).expect("path points to file") + } + }) + .collect::>(); + if !actions.iter().any(|action| action.kind == Action::Replace) { + actions.push(add_action); + } + actions + } + } + }) + .collect() }, working_directory, }) @@ -351,11 +336,13 @@ fn main() -> Result<(), String> { 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), - None => osf!(&file), + 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 scp_target = osf!(match &server.address { ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), @@ -548,6 +535,36 @@ where Ok(()) } +fn check_file_exists_on_server( + path: P, + ssh_address: S, + server_directory: D, +) -> Result<(), String> +where + P: AsRef, + S: AsRef, + D: AsRef, +{ + let full_path = server_directory.as_ref().join(path); + match &ShellCmd::new("ssh") + .arg(ssh_address.as_ref()) + .arg(osf!("test -f ") + &full_path) + .collect_output() + { + Ok(_) => Ok(()), //file exists on file server + Err(SpecificExecutionError { + error: ExecutionError::BadExitStatus(_), //test failed + .. + }) => Err(format!( + "File '{}' doesn't exist on file server", + full_path.to_string_lossy() + )), + Err(e) => Err(format!( + "Failed to check whether file exists on file-server: {e}" + )), + } +} + fn get_home_directory() -> Result { homedir::my_home() .map_err(|e| format!("Failed to determine home directory: {e}"))