Implement file uploading

This commit is contained in:
Leonard Steppy 2024-12-13 00:03:25 +01:00
parent f0f3216e01
commit dc9fb045f5
3 changed files with 288 additions and 24 deletions

57
src/action.rs Normal file
View File

@ -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<FileAction>
}
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,
}
}

View File

@ -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<String>,
pub ending: Option<String>,
pub extension: Option<String>,
}
impl TryFrom<PathBuf> for FileInfo {
impl FileNameInfo {
pub fn to_full_file_name(&self) -> String {
self.to_string()
}
}
impl TryFrom<PathBuf> for FileNameInfo {
type Error = FileInfoError;
fn try_from(file: PathBuf) -> Result<Self, Self::Error> {
@ -22,7 +28,7 @@ impl TryFrom<PathBuf> 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::<u32>().is_err() //there are usually no file extensions which are just a number, but rather versions
}) {
@ -38,12 +44,12 @@ impl TryFrom<PathBuf> 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<String>,
}
impl FileMatcher {
pub fn from<S>(name: S) -> Self
where
S: ToString,
{
Self {
name: name.to_string(),
extension: None,
}
}
pub fn and_extension<S>(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"));
}
}

View File

@ -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::<Result<Vec<_>, 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()?;