Implement file uploading
This commit is contained in:
parent
f0f3216e01
commit
dc9fb045f5
57
src/action.rs
Normal file
57
src/action.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
107
src/file.rs
107
src/file.rs
@ -3,13 +3,19 @@ use std::fmt::{Display, Formatter};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct FileInfo {
|
pub struct FileNameInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: Option<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;
|
type Error = FileInfoError;
|
||||||
|
|
||||||
fn try_from(file: PathBuf) -> Result<Self, Self::Error> {
|
fn try_from(file: PathBuf) -> Result<Self, Self::Error> {
|
||||||
@ -22,7 +28,7 @@ impl TryFrom<PathBuf> for FileInfo {
|
|||||||
.to_str()
|
.to_str()
|
||||||
.ok_or(FileInfoError::InvalidCharactersInFileName)?;
|
.ok_or(FileInfoError::InvalidCharactersInFileName)?;
|
||||||
|
|
||||||
let (file_name_without_version, ending) =
|
let (file_name_without_version, extension) =
|
||||||
match file_name.rsplit_once('.').filter(|(_, ending)| {
|
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
|
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 {
|
Ok(Self {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
version,
|
version,
|
||||||
ending,
|
extension,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for FileInfo {
|
impl Display for FileNameInfo {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
@ -55,9 +61,9 @@ impl Display for FileInfo {
|
|||||||
.map(|v| format!("-{v}"))
|
.map(|v| format!("-{v}"))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
self
|
self
|
||||||
.ending
|
.extension
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|e| format!(".{e}"))
|
.map(|extension| format!(".{extension}"))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -80,20 +86,56 @@ impl Display for FileInfoError {
|
|||||||
|
|
||||||
impl Error 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)]
|
#[cfg(test)]
|
||||||
mod test_file_info {
|
mod test_file_name_info {
|
||||||
use crate::file::FileInfo;
|
use crate::file::FileNameInfo;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_plugin() {
|
fn test_from_plugin() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
FileInfo {
|
FileNameInfo {
|
||||||
name: "TestPlugin".to_string(),
|
name: "TestPlugin".to_string(),
|
||||||
version: Some("1.0.0".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")
|
.expect("valid file")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -101,12 +143,12 @@ mod test_file_info {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_from_unversioned() {
|
fn test_from_unversioned() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
FileInfo {
|
FileNameInfo {
|
||||||
name: "unversioned".to_string(),
|
name: "unversioned".to_string(),
|
||||||
version: None,
|
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")
|
.expect("valid file")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -114,24 +156,45 @@ mod test_file_info {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_from_versioned_bin() {
|
fn test_from_versioned_bin() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
FileInfo {
|
FileNameInfo {
|
||||||
name: "bin".to_string(),
|
name: "bin".to_string(),
|
||||||
version: Some("7.3".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]
|
#[test]
|
||||||
fn test_from_unversioned_bin() {
|
fn test_from_unversioned_bin() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
FileInfo {
|
FileNameInfo {
|
||||||
name: "test".to_string(),
|
name: "test".to_string(),
|
||||||
version: None,
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
148
src/main.rs
148
src/main.rs
@ -1,12 +1,17 @@
|
|||||||
mod server;
|
mod action;
|
||||||
mod file;
|
mod file;
|
||||||
|
mod server;
|
||||||
|
|
||||||
|
use crate::action::{Action, FileAction, ServerActions};
|
||||||
|
use crate::file::{FileMatcher, FileNameInfo};
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
use lazy_regex::{lazy_regex, Lazy, Regex};
|
use lazy_regex::{lazy_regex, Lazy, Regex};
|
||||||
use server::{Server, ServerReference};
|
use server::{Server, ServerReference};
|
||||||
use std::cell::LazyCell;
|
use std::cell::LazyCell;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::iter::once;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@ -101,7 +106,146 @@ fn main() -> Result<(), String> {
|
|||||||
no_confirm,
|
no_confirm,
|
||||||
file_name,
|
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 } => {
|
Command::Command { command } => {
|
||||||
start_ssh_agent()?;
|
start_ssh_agent()?;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user