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}"))