Merge pull request '27 ShellCommand on localhost when calculating actions' (#28) from 27-shell-command-on-localhost-when-calculating-actions into master
Reviewed-on: https://stupstech.de/dev/Mr_Steppy/multi-ssh/pulls/28
This commit is contained in:
commit
8618e563dc
@ -72,7 +72,12 @@ pub enum Action {
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn rename<S>(new_name: S) -> Self where S: AsRef<OsStr> {
|
||||
Self::Rename {new_name: new_name.as_ref().to_owned()}
|
||||
pub fn rename<S>(new_name: S) -> Self
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Self::Rename {
|
||||
new_name: new_name.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
103
src/file.rs
103
src/file.rs
@ -1,17 +1,32 @@
|
||||
use crate::os_str_extension::OsStrExtension;
|
||||
use crate::osf;
|
||||
use std::error::Error;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FileNameInfo {
|
||||
pub name: String,
|
||||
pub version: Option<String>,
|
||||
pub extension: Option<String>,
|
||||
pub name: OsString,
|
||||
pub version: Option<OsString>,
|
||||
pub extension: Option<OsString>,
|
||||
}
|
||||
|
||||
impl FileNameInfo {
|
||||
pub fn to_full_file_name(&self) -> String {
|
||||
self.to_string()
|
||||
pub fn to_full_file_name(&self) -> OsString {
|
||||
(osf!(&self.name)
|
||||
+ self
|
||||
.version
|
||||
.as_ref()
|
||||
.map(|v| osf!("-") + v)
|
||||
.unwrap_or_default()
|
||||
+ self
|
||||
.extension
|
||||
.as_ref()
|
||||
.map(|extension| osf!(".") + extension)
|
||||
.unwrap_or_default())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,25 +35,27 @@ impl TryFrom<PathBuf> for FileNameInfo {
|
||||
|
||||
fn try_from(file: PathBuf) -> Result<Self, Self::Error> {
|
||||
let file_name = file.file_name().ok_or(FileInfoError::NotAFile)?;
|
||||
let file_name = file_name
|
||||
.to_str()
|
||||
.ok_or(FileInfoError::InvalidCharactersInFileName)?;
|
||||
|
||||
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
|
||||
let (file_name_without_extension, extension) =
|
||||
match file_name.rsplit_once(b'.').filter(|(_, ending)| {
|
||||
//there are usually no file extensions which are just a number, but rather versions
|
||||
// -> don't use split if ending is number
|
||||
match ending.to_str() {
|
||||
Some(ending) if ending.parse::<u32>().is_ok() => false,
|
||||
_ => true,
|
||||
}
|
||||
}) {
|
||||
Some((name, ending)) => (name, Some(ending.to_string())),
|
||||
Some((name, ending)) => (name, Some(ending.to_os_string())),
|
||||
None => (file_name, None),
|
||||
};
|
||||
|
||||
let (name, version) = match file_name_without_version.split_once('-') {
|
||||
Some((name, version)) => (name, Some(version.to_string())),
|
||||
None => (file_name_without_version, None),
|
||||
let (name, version) = match file_name_without_extension.split_once(b'-') {
|
||||
Some((name, version)) => (name, Some(version.to_os_string())),
|
||||
None => (file_name_without_extension, None),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
name: name.to_os_string(),
|
||||
version,
|
||||
extension,
|
||||
})
|
||||
@ -49,18 +66,8 @@ impl Display for FileNameInfo {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}{}{}",
|
||||
self.name,
|
||||
self
|
||||
.version
|
||||
.as_ref()
|
||||
.map(|v| format!("-{v}"))
|
||||
.unwrap_or_default(),
|
||||
self
|
||||
.extension
|
||||
.as_ref()
|
||||
.map(|extension| format!(".{extension}"))
|
||||
.unwrap_or_default()
|
||||
"{}",
|
||||
self.to_full_file_name().to_string_lossy()
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -84,37 +91,41 @@ impl Error for FileInfoError {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileMatcher {
|
||||
name: String,
|
||||
extension: Option<String>,
|
||||
name: OsString,
|
||||
extension: Option<OsString>,
|
||||
}
|
||||
|
||||
impl FileMatcher {
|
||||
pub fn from<S>(name: S) -> Self
|
||||
where
|
||||
S: ToString,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
name: name.as_ref().to_owned(),
|
||||
extension: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn and_extension<S>(self, extension: S) -> Self
|
||||
where
|
||||
S: ToString,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
Self {
|
||||
extension: Some(extension.to_string()),
|
||||
extension: Some(extension.as_ref().to_owned()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(&self, file_name: &str) -> bool {
|
||||
file_name.starts_with(&self.name)
|
||||
pub fn matches<S>(&self, file_name: S) -> bool
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
let file_name = file_name.as_ref();
|
||||
file_name.as_bytes().starts_with(self.name.as_bytes())
|
||||
&& self
|
||||
.extension
|
||||
.as_ref()
|
||||
.is_none_or(|extension| file_name.ends_with(extension))
|
||||
.is_none_or(|extension| file_name.as_bytes().ends_with(extension.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,9 +138,9 @@ mod test_file_name_info {
|
||||
fn test_from_plugin() {
|
||||
assert_eq!(
|
||||
FileNameInfo {
|
||||
name: "TestPlugin".to_string(),
|
||||
version: Some("1.0.0".to_string()),
|
||||
extension: Some("jar".to_string()),
|
||||
name: "TestPlugin".into(),
|
||||
version: Some("1.0.0".into()),
|
||||
extension: Some("jar".into()),
|
||||
},
|
||||
FileNameInfo::try_from(PathBuf::from("test-ressources/files/TestPlugin-1.0.0.jar"))
|
||||
.expect("valid file")
|
||||
@ -140,9 +151,9 @@ mod test_file_name_info {
|
||||
fn test_from_unversioned() {
|
||||
assert_eq!(
|
||||
FileNameInfo {
|
||||
name: "unversioned".to_string(),
|
||||
name: "unversioned".into(),
|
||||
version: None,
|
||||
extension: Some("jar".to_string()),
|
||||
extension: Some("jar".into()),
|
||||
},
|
||||
FileNameInfo::try_from(PathBuf::from("test-ressources/files/unversioned.jar"))
|
||||
.expect("valid file")
|
||||
@ -153,8 +164,8 @@ mod test_file_name_info {
|
||||
fn test_from_versioned_bin() {
|
||||
assert_eq!(
|
||||
FileNameInfo {
|
||||
name: "bin".to_string(),
|
||||
version: Some("7.3".to_string()),
|
||||
name: "bin".into(),
|
||||
version: Some("7.3".into()),
|
||||
extension: None,
|
||||
},
|
||||
FileNameInfo::try_from(PathBuf::from("test-ressources/files/bin-7.3")).expect("valid file")
|
||||
@ -165,7 +176,7 @@ mod test_file_name_info {
|
||||
fn test_from_unversioned_bin() {
|
||||
assert_eq!(
|
||||
FileNameInfo {
|
||||
name: "test".to_string(),
|
||||
name: "test".into(),
|
||||
version: None,
|
||||
extension: None,
|
||||
},
|
||||
@ -185,7 +196,7 @@ mod test_file_matcher {
|
||||
assert!(matcher.matches("test-1.3.0.jar"));
|
||||
assert!(!matcher.matches("test"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_match_without_extension() {
|
||||
let matcher = FileMatcher::from("test");
|
||||
|
||||
113
src/main.rs
113
src/main.rs
@ -3,23 +3,24 @@ mod command;
|
||||
mod file;
|
||||
mod logger;
|
||||
mod os_string_builder;
|
||||
mod os_str_extension;
|
||||
mod server;
|
||||
|
||||
use crate::action::{Action, FileAction, ServerActions};
|
||||
use crate::command::{CommandSpecificError, ExecutionError, LogRunnable};
|
||||
use crate::file::{FileMatcher, FileNameInfo};
|
||||
use crate::logger::{LogLevel, Logger};
|
||||
use crate::os_str_extension::OsStrExtension;
|
||||
use crate::os_string_builder::ReplaceWithOsStr;
|
||||
use crate::server::{RelativeLocalPathAnker, ServerAddress};
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use lazy_regex::{lazy_regex, Lazy, Regex};
|
||||
use server::{Server, ServerReference};
|
||||
use std::cell::LazyCell;
|
||||
use std::ffi::OsStr;
|
||||
use std::ffi::OsString;
|
||||
use std::hash::Hash;
|
||||
use std::io::Write;
|
||||
use std::iter::once;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::{env, fs, io};
|
||||
@ -232,11 +233,11 @@ fn main() -> Result<(), String> {
|
||||
))?;
|
||||
}
|
||||
|
||||
let denoted_files = output
|
||||
.stdout
|
||||
.split(|&b| b == b'\n') //split at line breaks
|
||||
let denoted_files = osstring_from_ssh_output(output.stdout)
|
||||
.split(b'\n') //split at line breaks
|
||||
.into_iter()
|
||||
.filter(|bytes| !bytes.is_empty()) //needed since realpath sometimes gives us empty lines
|
||||
.map(|bytes| PathBuf::from(OsStr::from_bytes(bytes)))
|
||||
.map(PathBuf::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(denoted_files)
|
||||
@ -292,28 +293,48 @@ fn main() -> Result<(), String> {
|
||||
Ok(ServerActions {
|
||||
server,
|
||||
actions: {
|
||||
let mut ls_command = match &server.address {
|
||||
ServerAddress::Ssh { ssh_address } => {
|
||||
let mut cmd = ShellCmd::new("ssh");
|
||||
cmd.arg(ssh_address).arg(osf!("ls ") + &working_directory);
|
||||
cmd
|
||||
}
|
||||
ServerAddress::Localhost => {
|
||||
let mut cmd = ShellCmd::new("ls");
|
||||
cmd.arg(&working_directory);
|
||||
cmd
|
||||
}
|
||||
let present_file_names: Vec<OsString> = match &server.address {
|
||||
ServerAddress::Ssh { ssh_address } => osstring_from_ssh_output(
|
||||
ShellCmd::new("ls")
|
||||
.arg(ssh_address)
|
||||
.arg(osf!("ls ") + &working_directory)
|
||||
.collect_output()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Failed to query present files on server {}: {e}",
|
||||
server.get_name()
|
||||
)
|
||||
})?
|
||||
.stdout,
|
||||
)
|
||||
.split(b'\n')
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect(),
|
||||
ServerAddress::Localhost => fs::read_dir(&working_directory)
|
||||
.map_err(|e| format!("Failed to get files in working directory: {e}"))?
|
||||
.map(|entry| entry.map_err(|e| format!("Failed to access directory entry: {e}")))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
if entry.path().is_file() {
|
||||
Some(entry.file_name())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let ls_output = ls_command
|
||||
.collect_output()
|
||||
.map_err(|e| format!("failed to query files: {e}"))?;
|
||||
let ls_output = String::from_utf8_lossy(&ls_output.stdout);
|
||||
|
||||
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));
|
||||
let mut file_matcher = FileMatcher::from(
|
||||
file_name
|
||||
.as_ref()
|
||||
.map(OsString::from)
|
||||
.unwrap_or(file_name_info.name.to_os_string()),
|
||||
);
|
||||
if let Some(extension) = file_name_info.extension.as_ref() {
|
||||
file_matcher = file_matcher.and_extension(extension);
|
||||
}
|
||||
@ -322,39 +343,53 @@ fn main() -> Result<(), String> {
|
||||
|
||||
let add_action = FileAction::new(file, Action::Add).expect("path points to file");
|
||||
|
||||
let mut ls_lines = ls_output.lines();
|
||||
|
||||
if pure && ls_lines.clone().any(|file| file == file_name) {
|
||||
log!(logger, debug, "file is already present on {}: {}", server.get_name(), file_name);
|
||||
if pure && present_file_names.iter().any(|file| *file == file_name) {
|
||||
log!(
|
||||
logger,
|
||||
debug,
|
||||
"file is already present on {}: {}",
|
||||
server.get_name(),
|
||||
file_name.to_string_lossy()
|
||||
);
|
||||
return vec![]; //ignore that file, since it is already present
|
||||
}
|
||||
|
||||
match old_version_policy {
|
||||
OldVersionPolicy::Ignore => {
|
||||
if !ls_lines.any(|file| file == file_name) {
|
||||
if !present_file_names.iter().any(|file| *file == file_name) {
|
||||
vec![add_action] //file doesn't exist yet
|
||||
} else {
|
||||
vec![FileAction::new(&file_name, Action::Replace)
|
||||
.expect("path points to file")]
|
||||
}
|
||||
}
|
||||
OldVersionPolicy::Archive => ls_lines
|
||||
OldVersionPolicy::Archive => present_file_names
|
||||
.iter()
|
||||
.filter(|file| file_matcher.matches(file))
|
||||
.map(|file| {
|
||||
FileAction::new(
|
||||
file,
|
||||
Action::rename(format!("{file}{}", file.chars().last().unwrap_or('1'))),
|
||||
Action::rename(
|
||||
osf!(file)
|
||||
+ file
|
||||
.to_string_lossy()
|
||||
.chars()
|
||||
.last()
|
||||
.unwrap_or('1')
|
||||
.to_string(),
|
||||
),
|
||||
)
|
||||
.expect("path points to file")
|
||||
})
|
||||
.chain(once(add_action))
|
||||
.collect(),
|
||||
OldVersionPolicy::Delete => {
|
||||
let mut actions = ls_lines
|
||||
let mut actions = present_file_names
|
||||
.iter()
|
||||
.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 {
|
||||
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")
|
||||
@ -589,6 +624,20 @@ fn main() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn osstring_from_ssh_output(output: Vec<u8>) -> OsString {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
OsString::from_vec(output)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
OsString::from_wide(output.iter().map(|&b| b as u16).collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn check_local_file_exists<P>(path: P) -> Result<(), String>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
|
||||
123
src/os_str_extension.rs
Normal file
123
src/os_str_extension.rs
Normal file
@ -0,0 +1,123 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
pub trait OsStrExtension {
|
||||
fn starts_with<S>(&self, prefix: S) -> bool
|
||||
where
|
||||
S: AsRef<OsStr>;
|
||||
fn ends_with<S>(&self, suffix: S) -> bool
|
||||
where
|
||||
S: AsRef<OsStr>;
|
||||
fn split(&self, separator: u8) -> Vec<&OsStr>;
|
||||
fn splitn(&self, n: usize, separator: u8) -> Vec<&OsStr>;
|
||||
fn split_once(&self, separator: u8) -> Option<(&OsStr, &OsStr)>;
|
||||
fn rsplitn(&self, n: usize, separator: u8) -> Vec<&OsStr>;
|
||||
fn rsplit_once(&self, separator: u8) -> Option<(&OsStr, &OsStr)>;
|
||||
}
|
||||
|
||||
impl<T> OsStrExtension for T
|
||||
where
|
||||
T: AsRef<OsStr>,
|
||||
{
|
||||
fn starts_with<S>(&self, prefix: S) -> bool
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
self
|
||||
.as_ref()
|
||||
.as_encoded_bytes()
|
||||
.starts_with(prefix.as_ref().as_encoded_bytes())
|
||||
}
|
||||
|
||||
fn ends_with<S>(&self, suffix: S) -> bool
|
||||
where
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
self
|
||||
.as_ref()
|
||||
.as_encoded_bytes()
|
||||
.ends_with(suffix.as_ref().as_encoded_bytes())
|
||||
}
|
||||
|
||||
fn split(&self, separator: u8) -> Vec<&OsStr> {
|
||||
self
|
||||
.as_ref()
|
||||
.as_encoded_bytes()
|
||||
.split(|&b| b == separator)
|
||||
.map(|bytes| unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn splitn(&self, n: usize, separator: u8) -> Vec<&OsStr> {
|
||||
self
|
||||
.as_ref()
|
||||
.as_encoded_bytes()
|
||||
.splitn(n, |&b| b == separator)
|
||||
.map(|bytes| unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn split_once(&self, separator: u8) -> Option<(&OsStr, &OsStr)> {
|
||||
let mut iter = self.splitn(2, separator).into_iter();
|
||||
Some((iter.next()?, iter.next()?))
|
||||
}
|
||||
|
||||
fn rsplitn(&self, n: usize, separator: u8) -> Vec<&OsStr> {
|
||||
self
|
||||
.as_ref()
|
||||
.as_encoded_bytes()
|
||||
.rsplitn(n, |&b| b == separator)
|
||||
.map(|bytes| unsafe { OsStr::from_encoded_bytes_unchecked(bytes) })
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn rsplit_once(&self, separator: u8) -> Option<(&OsStr, &OsStr)> {
|
||||
let mut iter = self.rsplitn(2, separator).into_iter();
|
||||
Some((iter.next()?, iter.next()?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::os_str_extension::OsStrExtension;
|
||||
use std::ffi::OsString;
|
||||
|
||||
#[test]
|
||||
fn test_starts_with() {
|
||||
assert!(OsString::from("Hello world").starts_with("Hell"));
|
||||
assert!(!OsString::from("Hello world").starts_with("world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ends_with() {
|
||||
assert!(OsString::from("Hello world").ends_with("ld"));
|
||||
assert!(!OsString::from("Hello world").ends_with("Hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split() {
|
||||
assert_eq!(vec![OsString::from("Hello"), OsString::from("world")], OsString::from("Hello world").split(b' '));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_splitn() {
|
||||
assert_eq!(vec![OsString::from("a"), OsString::from("b"), OsString::from("c d")], OsString::from("a b c d").splitn(3, b' '));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_once() {
|
||||
assert_eq!(Some((OsString::from("a").as_ref(), OsString::from("b c").as_ref())), OsString::from("a b c").split_once(b' '));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rsplitn() {
|
||||
assert_eq!(vec![OsString::from("a b"), OsString::from("c"), OsString::from("d")], OsString::from("a b c d").rsplitn(3, b' '));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rsplit_once() {
|
||||
assert_eq!(Some((OsString::from("a b").as_ref(), OsString::from("c").as_ref())), OsString::from("a b c").rsplit_once(b' '));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user