From dc9fb045f51ebcf59daded2bc2025718973a5725 Mon Sep 17 00:00:00 2001 From: Steppy Date: Fri, 13 Dec 2024 00:03:25 +0100 Subject: [PATCH] Implement file uploading --- src/action.rs | 57 +++++++++++++++++++ src/file.rs | 107 ++++++++++++++++++++++++++++-------- src/main.rs | 148 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 288 insertions(+), 24 deletions(-) create mode 100644 src/action.rs diff --git a/src/action.rs b/src/action.rs new file mode 100644 index 0000000..8f15b42 --- /dev/null +++ b/src/action.rs @@ -0,0 +1,57 @@ +use crate::server::Server; +use std::ffi::OsString; +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct ServerActions<'a> { + pub server: &'a Server, + pub actions: Vec +} + +impl <'a> ServerActions<'a> { + pub fn new(server: &'a Server) -> Self { + Self { + server, + actions: vec![], + } + } +} + +impl Display for ServerActions<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: ", self.server.ssh_name)?; + for action in &self.actions { + write!(f, "\n{}", action)?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FileAction { + pub file: PathBuf, + pub kind: Action, +} + +impl Display for FileAction { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.kind { + Action::Add => write!(f, "+ adding {:?}", self.file), + Action::Replace => write!(f, "~ replacing {:?}", self.file), + Action::Delete => write!(f, "- deleting {:?}", self.file), + Action::Rename { new_name } => write!(f, "* renaming {:?} -> {:?}", self.file, new_name), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum Action { + Add, + Replace, + Delete, + Rename { + new_name: OsString, + } +} + diff --git a/src/file.rs b/src/file.rs index fedf7b9..161631b 100644 --- a/src/file.rs +++ b/src/file.rs @@ -3,13 +3,19 @@ use std::fmt::{Display, Formatter}; use std::path::PathBuf; #[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct FileInfo { +pub struct FileNameInfo { pub name: String, pub version: Option, - pub ending: Option, + pub extension: Option, } -impl TryFrom for FileInfo { +impl FileNameInfo { + pub fn to_full_file_name(&self) -> String { + self.to_string() + } +} + +impl TryFrom for FileNameInfo { type Error = FileInfoError; fn try_from(file: PathBuf) -> Result { @@ -22,7 +28,7 @@ impl TryFrom for FileInfo { .to_str() .ok_or(FileInfoError::InvalidCharactersInFileName)?; - let (file_name_without_version, ending) = + let (file_name_without_version, extension) = match file_name.rsplit_once('.').filter(|(_, ending)| { ending.parse::().is_err() //there are usually no file extensions which are just a number, but rather versions }) { @@ -38,12 +44,12 @@ impl TryFrom for FileInfo { Ok(Self { name: name.to_string(), version, - ending, + extension, }) } } -impl Display for FileInfo { +impl Display for FileNameInfo { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -55,9 +61,9 @@ impl Display for FileInfo { .map(|v| format!("-{v}")) .unwrap_or_default(), self - .ending + .extension .as_ref() - .map(|e| format!(".{e}")) + .map(|extension| format!(".{extension}")) .unwrap_or_default() ) } @@ -80,20 +86,56 @@ impl Display for FileInfoError { impl Error for FileInfoError {} +#[derive(Debug, Clone)] +pub struct FileMatcher { + name: String, + extension: Option, +} + +impl FileMatcher { + pub fn from(name: S) -> Self + where + S: ToString, + { + Self { + name: name.to_string(), + extension: None, + } + } + + pub fn and_extension(self, extension: S) -> Self + where + S: ToString, + { + Self { + extension: Some(extension.to_string()), + ..self + } + } + + pub fn matches(&self, file_name: &str) -> bool { + file_name.starts_with(&self.name) + && self + .extension + .as_ref() + .is_none_or(|extension| file_name.ends_with(extension)) + } +} + #[cfg(test)] -mod test_file_info { - use crate::file::FileInfo; +mod test_file_name_info { + use crate::file::FileNameInfo; use std::path::PathBuf; #[test] fn test_from_plugin() { assert_eq!( - FileInfo { + FileNameInfo { name: "TestPlugin".to_string(), version: Some("1.0.0".to_string()), - ending: Some("jar".to_string()), + extension: Some("jar".to_string()), }, - FileInfo::try_from(PathBuf::from("test-ressources/files/TestPlugin-1.0.0.jar")) + FileNameInfo::try_from(PathBuf::from("test-ressources/files/TestPlugin-1.0.0.jar")) .expect("valid file") ); } @@ -101,12 +143,12 @@ mod test_file_info { #[test] fn test_from_unversioned() { assert_eq!( - FileInfo { + FileNameInfo { name: "unversioned".to_string(), version: None, - ending: Some("jar".to_string()), + extension: Some("jar".to_string()), }, - FileInfo::try_from(PathBuf::from("test-ressources/files/unversioned.jar")) + FileNameInfo::try_from(PathBuf::from("test-ressources/files/unversioned.jar")) .expect("valid file") ); } @@ -114,24 +156,45 @@ mod test_file_info { #[test] fn test_from_versioned_bin() { assert_eq!( - FileInfo { + FileNameInfo { name: "bin".to_string(), version: Some("7.3".to_string()), - ending: None, + extension: None, }, - FileInfo::try_from(PathBuf::from("test-ressources/files/bin-7.3")).expect("valid file") + FileNameInfo::try_from(PathBuf::from("test-ressources/files/bin-7.3")).expect("valid file") ); } #[test] fn test_from_unversioned_bin() { assert_eq!( - FileInfo { + FileNameInfo { name: "test".to_string(), version: None, - ending: None, + extension: None, }, - FileInfo::try_from(PathBuf::from("test-ressources/files/test")).expect("valid file") + FileNameInfo::try_from(PathBuf::from("test-ressources/files/test")).expect("valid file") ); } } + +#[cfg(test)] +mod test_file_matcher { + use crate::file::FileMatcher; + + #[test] + fn test_match_with_extension() { + let matcher = FileMatcher::from("test").and_extension(".jar"); + assert!(matcher.matches("test.jar")); + assert!(matcher.matches("test-1.3.0.jar")); + assert!(!matcher.matches("test")); + } + + #[test] + fn test_match_without_extension() { + let matcher = FileMatcher::from("test"); + assert!(matcher.matches("test.jar")); + assert!(matcher.matches("test-1.3.0.jar")); + assert!(matcher.matches("test")); + } +} diff --git a/src/main.rs b/src/main.rs index eb8458f..61ec754 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,17 @@ -mod server; +mod action; mod file; +mod server; +use crate::action::{Action, FileAction, ServerActions}; +use crate::file::{FileMatcher, FileNameInfo}; use clap::{Parser, Subcommand, ValueEnum}; use lazy_regex::{lazy_regex, Lazy, Regex}; use server::{Server, ServerReference}; use std::cell::LazyCell; use std::env; use std::hash::Hash; +use std::io::Write; +use std::iter::once; use std::path::PathBuf; use std::process::Stdio; use std::str::FromStr; @@ -101,7 +106,146 @@ fn main() -> Result<(), String> { no_confirm, file_name, } => { - todo!() + start_ssh_agent()?; + + let file_name_info = + FileNameInfo::try_from(file.clone()).map_err(|e| format!("bad file: {e}"))?; + + //create overview of what has to be done on each server + let actions = servers + .iter() + .map(|server| { + Ok(ServerActions { + server, + actions: { + let output = ShellCmd::new("ssh") + .arg(&server.ssh_name) + .arg(format!( + "cd {:?}; ls {:?}", + server.server_directory_path, upload_directory + )) + .stdout(Stdio::piped()) + .output() + .map_err(|e| format!("failed to query files via ssh: {e}"))?; + let output = String::from_utf8_lossy(&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 add_action_iter = once(FileAction { + file: file.clone(), + kind: Action::Add, + }); + let mut files = output.lines(); + match old_version_policy { + OldVersionPolicy::Ignore => { + let file_name = file_name_info.to_full_file_name(); + vec![if files.any(|file| file == file_name) { + FileAction { + file: PathBuf::from(file_name), + kind: Action::Replace, + } + } else { + FileAction { + file: file.clone(), + kind: Action::Add, + } + }] + } + 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(add_action_iter) + .collect(), + OldVersionPolicy::Delete => files + .filter(|file| file_matcher.matches(file)) + .map(|file| FileAction { + file: PathBuf::from(file), + kind: Action::Delete, + }) + .chain(add_action_iter) + .collect(), + } + }, + }) + }) + .collect::, String>>()?; + + println!("The following actions will be performed:"); + for server_actions in &actions { + println!("{server_actions}"); + } + + if !no_confirm { + print!("Continue? [Y|n] "); + std::io::stdout().flush().expect("failed to flush stdout"); + let mut buffer = String::new(); + std::io::stdin() + .read_line(&mut buffer) + .expect("failed to read stdin"); + match buffer.to_lowercase().trim() { + "n" | "no" => { + println!("Aborting..."); + return Ok(()); + } + _ => {} + } + } + + for server_actions in actions { + let server = server_actions.server; + println!("Performing actions on {}...", server.ssh_name); + for file_action in server_actions.actions { + match file_action.kind { + Action::Add | Action::Replace => { + ShellCmd::new("scp") + .arg(file.clone()) + .arg(format!( + "{}:{:?}/{upload_directory:?}", + server.ssh_name, server.server_directory_path + )) + .spawn() + .map_err(|e| format!("failed to upload file: {e}"))? + .wait() + .map_err(|e| format!("failed to wait for upload: {e}"))?; + } + Action::Delete => { + ShellCmd::new("ssh") + .arg(&server.ssh_name) + .arg(format!( + "cd {:?}; cd {upload_directory:?}; rm {:?}", + server.server_directory_path, file_action.file + )) + .spawn() + .map_err(|e| format!("failed to send delete command: {e}"))? + .wait() + .map_err(|e| format!("failed to wait for delete command: {e}"))?; + } + Action::Rename { new_name } => { + ShellCmd::new("ssh") + .arg(&server.ssh_name) + .arg(format!( + "cd {:?}; cd {upload_directory:?}; mv {:?} {new_name:?}", + server.server_directory_path, file_action.file + )) + .spawn() + .map_err(|e| format!("failed to send rename command: {e}"))? + .wait() + .map_err(|e| format!("failed to wait for rename command: {e}"))?; + } + } + } + } + + println!("Done!") } Command::Command { command } => { start_ssh_agent()?;