Merge pull request 'Support uploading multiple files' (#19) from 13-allow-multiple-files-in-upload-command into master

Reviewed-on: https://stupstech.de/dev/Mr_Steppy/multi-ssh/pulls/19
This commit is contained in:
Leonard Steppy 2025-02-02 22:07:09 +01:00
commit f2f7f3bae9
3 changed files with 152 additions and 106 deletions

View File

@ -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):

View File

@ -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<P>(file: P, kind: Action) -> Option<Self>
where
P: AsRef<Path>,
{
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<S>(new_name: S) -> Self where S: AsRef<OsStr> {
Self::Rename {new_name: new_name.as_ref().to_owned()}
}
}

View File

@ -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<PathBuf>,
/// 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::<Result<Vec<_>, _>>()?;
//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::<Vec<_>>();
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<P, S, D>(
path: P,
ssh_address: S,
server_directory: D,
) -> Result<(), String>
where
P: AsRef<Path>,
S: AsRef<str>,
D: AsRef<Path>,
{
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<PathBuf, String> {
homedir::my_home()
.map_err(|e| format!("Failed to determine home directory: {e}"))