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:
commit
f2f7f3bae9
@ -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
|
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:
|
The program will show you an overview of what it will be doing before actually performing any of the actions:
|
||||||
```
|
```
|
||||||
minez (minez3/plugins):
|
minez (minez3/plugins):
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
use std::ffi::OsString;
|
use std::ffi::{OsStr, OsString};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ServerActions<'a> {
|
pub struct ServerActions<'a> {
|
||||||
@ -12,7 +12,12 @@ pub struct ServerActions<'a> {
|
|||||||
|
|
||||||
impl Display for ServerActions<'_> {
|
impl Display for ServerActions<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
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 {
|
for action in &self.actions {
|
||||||
write!(f, "\n{}", action)?;
|
write!(f, "\n{}", action)?;
|
||||||
}
|
}
|
||||||
@ -23,19 +28,35 @@ impl Display for ServerActions<'_> {
|
|||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct FileAction {
|
pub struct FileAction {
|
||||||
pub file: PathBuf,
|
pub file: PathBuf,
|
||||||
|
pub file_name: OsString,
|
||||||
pub kind: Action,
|
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 {
|
impl Display for FileAction {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
Action::Add => write!(f, "+ adding {}", self.file.to_string_lossy()),
|
Action::Add => write!(f, "+ adding {}", self.file_name.to_string_lossy()),
|
||||||
Action::Replace => write!(f, "~ replacing {}", self.file.to_string_lossy()),
|
Action::Replace => write!(f, "~ replacing {}", self.file_name.to_string_lossy()),
|
||||||
Action::Delete => write!(f, "- deleting {}", self.file.to_string_lossy()),
|
Action::Delete => write!(f, "- deleting {}", self.file_name.to_string_lossy()),
|
||||||
Action::Rename { new_name } => write!(
|
Action::Rename { new_name } => write!(
|
||||||
f,
|
f,
|
||||||
"* renaming {} -> {}",
|
"* renaming {} -> {}",
|
||||||
self.file.to_string_lossy(),
|
self.file_name.to_string_lossy(),
|
||||||
new_name.to_string_lossy()
|
new_name.to_string_lossy()
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -49,3 +70,9 @@ pub enum Action {
|
|||||||
Delete,
|
Delete,
|
||||||
Rename { new_name: OsString },
|
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()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
215
src/main.rs
215
src/main.rs
@ -6,7 +6,7 @@ mod os_string_builder;
|
|||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
use crate::action::{Action, FileAction, ServerActions};
|
use crate::action::{Action, FileAction, ServerActions};
|
||||||
use crate::command::{ExecutionError, LogRunnable};
|
use crate::command::{ExecutionError, LogRunnable, SpecificExecutionError};
|
||||||
use crate::file::{FileMatcher, FileNameInfo};
|
use crate::file::{FileMatcher, FileNameInfo};
|
||||||
use crate::logger::{LogLevel, Logger};
|
use crate::logger::{LogLevel, Logger};
|
||||||
use crate::os_string_builder::ReplaceWithOsStr;
|
use crate::os_string_builder::ReplaceWithOsStr;
|
||||||
@ -61,8 +61,8 @@ enum Command {
|
|||||||
/// Upload a file to the servers
|
/// Upload a file to the servers
|
||||||
#[command(visible_short_flag_alias = 'u')]
|
#[command(visible_short_flag_alias = 'u')]
|
||||||
Upload {
|
Upload {
|
||||||
/// The file to upload
|
/// The files to upload
|
||||||
file: PathBuf,
|
files: Vec<PathBuf>,
|
||||||
/// The ssh server to get the file from.
|
/// 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.
|
/// 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 {
|
match args.command {
|
||||||
Command::Upload {
|
Command::Upload {
|
||||||
file,
|
files,
|
||||||
file_server,
|
file_server,
|
||||||
old_version_policy,
|
old_version_policy,
|
||||||
upload_directory,
|
upload_directory,
|
||||||
@ -200,45 +200,35 @@ fn main() -> Result<(), String> {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
//make sure file exists and is a file
|
//make sure files exist
|
||||||
match &file_server {
|
match &file_server {
|
||||||
Some(file_server) => {
|
Some(file_server) => match &file_server.address {
|
||||||
match &file_server.address {
|
ServerAddress::Ssh { ssh_address } => {
|
||||||
ServerAddress::Ssh { ssh_address } => {
|
for file in &files {
|
||||||
match ShellCmd::new("ssh")
|
check_file_exists_on_server(file, ssh_address, &file_server.server_directory_path)?;
|
||||||
.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))?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
ServerAddress::Localhost => {
|
||||||
|
for file in &files {
|
||||||
|
check_local_file_exists(file_server.server_directory_path.join(file))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
None => {
|
None => {
|
||||||
check_local_file_exists(&file)?;
|
for file in &files {
|
||||||
|
check_local_file_exists(file)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_name_info =
|
let file_details = files
|
||||||
FileNameInfo::try_from(file.clone()).map_err(|e| format!("bad file: {e}"))?;
|
.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
|
//create overview of what has to be done on each server
|
||||||
let actions = servers
|
let actions = servers
|
||||||
@ -263,66 +253,61 @@ fn main() -> Result<(), String> {
|
|||||||
let ls_output = ls_command
|
let ls_output = ls_command
|
||||||
.collect_output()
|
.collect_output()
|
||||||
.map_err(|e| format!("failed to query files: {e}"))?;
|
.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 =
|
file_details
|
||||||
FileMatcher::from(file_name.as_ref().unwrap_or(&file_name_info.name));
|
.iter()
|
||||||
if let Some(extension) = file_name_info.extension.as_ref() {
|
.flat_map(|(file, file_name_info)| {
|
||||||
file_matcher = file_matcher.and_extension(extension);
|
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() {
|
||||||
let file_name = file_name_info.to_full_file_name();
|
file_matcher = file_matcher.and_extension(extension);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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,
|
working_directory,
|
||||||
})
|
})
|
||||||
@ -351,11 +336,13 @@ fn main() -> Result<(), String> {
|
|||||||
match file_action.kind {
|
match file_action.kind {
|
||||||
Action::Add | Action::Replace => {
|
Action::Add | Action::Replace => {
|
||||||
let scp_source = match &file_server {
|
let scp_source = match &file_server {
|
||||||
Some(file_server) => osf!(match &file_server.address {
|
Some(file_server) => {
|
||||||
ServerAddress::Ssh{ ssh_address } => format!("{ssh_address}:"),
|
osf!(match &file_server.address {
|
||||||
ServerAddress::Localhost => "".to_string(),
|
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
|
||||||
}) + file_server.server_directory_path.join(&file),
|
ServerAddress::Localhost => "".to_string(),
|
||||||
None => osf!(&file),
|
}) + file_server.server_directory_path.join(&file_action.file)
|
||||||
|
}
|
||||||
|
None => osf!(&file_action.file),
|
||||||
};
|
};
|
||||||
let scp_target = osf!(match &server.address {
|
let scp_target = osf!(match &server.address {
|
||||||
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
|
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
|
||||||
@ -548,6 +535,36 @@ where
|
|||||||
Ok(())
|
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> {
|
fn get_home_directory() -> Result<PathBuf, String> {
|
||||||
homedir::my_home()
|
homedir::my_home()
|
||||||
.map_err(|e| format!("Failed to determine home directory: {e}"))
|
.map_err(|e| format!("Failed to determine home directory: {e}"))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user