diff --git a/README.md b/README.md index d89d39f..a411e5b 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,15 @@ Ergo you should be able to connect to your desired servers via ssh and be able t ### Environment variable setup example for linux ```bash -export MSSH_SERVERS="crea:server/creative2,sky:sky,city:city2" +export MSSH_SERVERS="crea:server/creative2,sky:sky,city:city2,.:minecraft-server" export MSSH_EDITOR="kate -b " # is the placeholder for the file name ``` +### localhost as server-target + +You may also use `.` to refer to your local minecraft server, without having to open the ssh port. You still have to +define the server directory though. + ## Usage For detailed usage please see: @@ -80,4 +85,4 @@ and you will find an executable in `target/release`. ### Unit tests -In order for the unit tests to pass, you will need `python3` +In order for the unit tests to pass, you will need `python3`. diff --git a/src/action.rs b/src/action.rs index e7538b5..c589629 100644 --- a/src/action.rs +++ b/src/action.rs @@ -12,7 +12,7 @@ pub struct ServerActions<'a> { impl Display for ServerActions<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}: ({})", self.server.ssh_name, self.working_directory.to_string_lossy())?; + write!(f, "{}: ({})", self.server.get_name(), self.working_directory.to_string_lossy())?; for action in &self.actions { write!(f, "\n{}", action)?; } diff --git a/src/command.rs b/src/command.rs index 4b9073d..b301a1f 100644 --- a/src/command.rs +++ b/src/command.rs @@ -49,6 +49,7 @@ fn collect_output( let output = command.output()?; //pipes stdout and stderr automatically if !output.status.success() { if let Some(logger) = logger { + log!(logger, error, "{}", String::from_utf8_lossy(&output.stdout)); log!(logger, error, "{}", String::from_utf8_lossy(&output.stderr)); } Err(output.status)?; diff --git a/src/logger.rs b/src/logger.rs index 57ddae5..93d29c7 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -7,14 +7,20 @@ pub struct Logger { macro_rules! define_log_function { ($name:ident, $level:ident) => { - pub fn $name(&self, message: S) where S: ToString { - self.log(LogLevel::$level, message.to_string()); - } + pub fn $name(&self, message: S) + where + S: ToString, + { + self.log(LogLevel::$level, message.to_string()); + } }; } impl Logger { - pub fn log(&self, level: LogLevel, message: S) where S: ToString { + pub fn log(&self, level: LogLevel, message: S) + where + S: ToString, + { if level >= self.level { println!("{}", message.to_string()); } @@ -38,9 +44,10 @@ macro_rules! log { ($logger:expr, $level:ident, $($args:tt)*) => { $logger.$level(format!($($args)*)); }; - ($logger:expr, $($args:tt)*) => { - log!($logger, info, $($args)*); //TODO better use default level with log function instead of assuming info as default - } + ($logger:expr, $($args:tt)*) => {{ + use $crate::logger::LogLevel; + $logger.log(LogLevel::default(), format!($($args)*)); + }}; } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index 4c791ed..c595e9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use crate::command::LogRunnable; use crate::file::{FileMatcher, FileNameInfo}; use crate::logger::{LogLevel, Logger}; use crate::os_string_builder::ReplaceWithOsStr; +use crate::server::ServerAddress; use clap::{Parser, Subcommand, ValueEnum}; use lazy_regex::{lazy_regex, Lazy, Regex}; use server::{Server, ServerReference}; @@ -156,7 +157,7 @@ fn main() -> Result<(), String> { .servers .iter() .map(|server_reference| { - let server_name = server_reference.get_name(); + let server_name = server_reference.get_identifier(); server_reference .clone() .try_resolve_lazy(&mut configured_servers) @@ -191,12 +192,22 @@ fn main() -> Result<(), String> { Ok(ServerActions { server, actions: { - let output = ShellCmd::new("ssh") - .arg(&server.ssh_name) - .arg(osf!("ls ") + &working_directory) + 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 ls_output = ls_command .collect_output() .map_err(|e| format!("failed to query files: {e}"))?; - let output = String::from_utf8_lossy(&output.stdout); + let output = String::from_utf8_lossy(&ls_output.stdout); let mut file_matcher = FileMatcher::from(file_name.as_ref().unwrap_or(&file_name_info.name)); @@ -279,37 +290,52 @@ fn main() -> Result<(), String> { for server_actions in actions { let server = server_actions.server; - log!(logger, "Performing actions on {}...", server.ssh_name); + log!(logger, "Performing actions on {}...", server.get_name()); for file_action in server_actions.actions { match file_action.kind { Action::Add | Action::Replace => { + let scp_target = osf!(match &server.address { + ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), + ServerAddress::Localhost => "".to_string(), + }) + &server_actions.working_directory; ShellCmd::new("scp") .arg(file.clone()) - .arg(osf!(&server.ssh_name) + ":" + &server_actions.working_directory) + .arg(scp_target) .run(&logger) .map_err(|e| format!("upload failure: {e}"))?; } - Action::Delete => { - ShellCmd::new("ssh") - .arg(&server.ssh_name) - .arg(osf!("cd ") + &server_actions.working_directory + "; rm " + &file_action.file) - .run(&logger) - .map_err(|e| format!("failed to delete old version: {e}"))?; - } - Action::Rename { new_name } => { - ShellCmd::new("ssh") - .arg(&server.ssh_name) - .arg( - osf!("cd ") - + &server_actions.working_directory - + "; mv " - + &file_action.file - + " " - + new_name, - ) - .run(&logger) - .map_err(|e| format!("failed to rename: {e}"))?; - } + Action::Delete => match &server.address { + ServerAddress::Ssh { ssh_address } => { + ShellCmd::new("ssh") + .arg(ssh_address) + .arg(osf!("rm ") + server_actions.working_directory.join(&file_action.file)) + .run(&logger) + .map_err(|e| format!("failed to delete old version: {e}"))?; + } + ServerAddress::Localhost => { + fs::remove_file(server_actions.working_directory.join(&file_action.file)) + .map_err(|e| format!("failed to delete old version: {e}"))?; + } + }, + Action::Rename { new_name } => match &server.address { + ServerAddress::Ssh { ssh_address } => { + ShellCmd::new("ssh") + .arg(ssh_address) + .arg( + osf!("mv ") + + server_actions.working_directory.join(&file_action.file) + + " " + + server_actions.working_directory.join(&new_name), + ) + .run(&logger) + .map_err(|e| format!("failed to rename: {e}"))?; + } + ServerAddress::Localhost => { + let dir = &server_actions.working_directory; + fs::rename(dir.join(&file_action.file), dir.join(&new_name)) + .map_err(|e| format!("failed to rename: {e}"))?; + } + }, } } } @@ -320,12 +346,25 @@ fn main() -> Result<(), String> { start_ssh_agent(&logger)?; require_non_empty_servers(&servers)?; for server in servers { - log!(logger, "Running command on '{}'...", server.ssh_name); - ShellCmd::new("ssh") - .arg(server.ssh_name) - .arg(osf!("cd ") + server.server_directory_path + "; " + &command) - .run(&logger) - .map_err(|e| format!("{e}"))?; + log!(logger, "Running command on '{}'...", server.get_name()); + match &server.address { + ServerAddress::Ssh { ssh_address } => { + ShellCmd::new("ssh") + .arg(ssh_address) + .arg(osf!("cd ") + server.server_directory_path + "; " + &command) + .run(&logger) + .map_err(|e| format!("{e}"))?; + } + ServerAddress::Localhost => { + let mut command_args = + shell_words::split(&command).map_err(|e| format!("failed to parse command: {e}"))?; + ShellCmd::new(command_args.remove(0)) + .args(&command_args) + .current_dir(&server.server_directory_path) + .run(&logger) + .map_err(|e| format!("{e}"))?; + } + } } log!(logger, "Done!"); } @@ -339,12 +378,8 @@ fn main() -> Result<(), String> { let download_directory = match download_directory { Some(download_directory) => download_directory, None => { - let home_dir = homedir::my_home() - .map_err(|e| format!("Failed to determine your home directory: {e}")) - .and_then(|home_dir| { - home_dir.ok_or("Failed to determine your home directory".to_string()) - }) - .map_err(|e| format!("Can't determine download directory: {e}"))?; + let home_dir = + get_home_directory().map_err(|e| format!("Can't determine download directory: {e}"))?; home_dir.join("Downloads") } }; @@ -355,7 +390,7 @@ fn main() -> Result<(), String> { fs::create_dir_all(&download_directory) .map_err(|e| format!("failed to create working directory: {e}"))?; - //make sure file doesn't exist in working directory yet, or will be overridden + //make sure file doesn't exist in working directory yet, or it will be overridden let file_name = file .file_name() .ok_or("can only edit files, not directories")?; @@ -394,15 +429,19 @@ fn main() -> Result<(), String> { start_ssh_agent(&logger)?; for server in servers { - log!(logger, "Downloading file from {}...", server.ssh_name); + log!(logger, "Getting file from {}...", server.get_name()); + let file_source = osf!(match &server.address { + ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), + ServerAddress::Localhost => "".to_string(), + }) + server.server_directory_path.join(&file); ShellCmd::new("scp") - .arg(osf!(&server.ssh_name) + ":" + server.server_directory_path.join(&file)) + .arg(&file_source) .arg(&download_directory) .run(&logger) .map_err(|e| format!("download failure: {e}"))?; //open file in editor - let mut shell_args = shell_words::split(&editor) + let mut editor_command_args = shell_words::split(&editor) .map_err(|e| format!("failed to parse editor command: {e}"))? .into_iter() .map(|part| { @@ -410,16 +449,16 @@ fn main() -> Result<(), String> { }) .collect::>(); - let command = shell_args.remove(0); + let command = editor_command_args.remove(0); ShellCmd::new(command) - .args(shell_args) + .args(editor_command_args) .run(&logger) .map_err(|e| format!("failed to open file in editor: {e}"))?; //upload file again ShellCmd::new("scp") .arg(download_directory.join(file_name)) - .arg(osf!(&server.ssh_name) + ":" + server.server_directory_path.join(&file)) + .arg(&file_source) .run(&logger) .map_err(|e| format!("failed to re-upload file: {e}"))?; } @@ -431,6 +470,12 @@ fn main() -> Result<(), String> { Ok(()) } +fn get_home_directory() -> Result { + homedir::my_home() + .map_err(|e| format!("Failed to determine home directory: {e}")) + .and_then(|home_dir| home_dir.ok_or("Failed to find home directory".to_string())) +} + fn require_non_empty_servers(servers: &[Server]) -> Result<(), String> { if servers.is_empty() { Err("You did not provide any servers for this operation. Please see --help".to_string()) @@ -483,28 +528,32 @@ fn parse_server_configuration(config_str: &str) -> Result, String> { #[cfg(test)] mod test { use crate::parse_server_configuration; - use crate::server::Server; + use crate::server::{Server, ServerAddress}; + use std::fs; use std::path::PathBuf; #[test] fn test_parse_server_configuration() { let servers = - parse_server_configuration("foo:bar,fizz:buzz/bizz").expect("valid server configuration"); + parse_server_configuration("foo:bar,.:fizz/buzz").expect("valid server configuration"); assert_eq!( vec![ Server { - ssh_name: "foo".to_string(), + address: ServerAddress::Ssh { + ssh_address: "foo".to_string() + }, server_directory_path: PathBuf::from("bar"), }, Server { - ssh_name: "fizz".to_string(), - server_directory_path: PathBuf::from("buzz/bizz"), + address: ServerAddress::Localhost, + server_directory_path: PathBuf::from("fizz/buzz"), } ], servers ); } + /// When we join an absolute path to a relative path, it becomes a relative path #[test] fn path_experiment() { let server_dir = PathBuf::from("steptech"); @@ -513,4 +562,12 @@ mod test { let joined = server_dir.join(upload_dir); assert_eq!(PathBuf::from("/home"), joined); } + + #[test] + fn rename_experiment() { + fs::rename("test-ressources/files/test", "test-ressources/files/test1") + .expect("failed to rename test file"); + fs::rename("test-ressources/files/test1", "test-ressources/files/test") + .expect("failed to rename test1 file back to test"); + } } diff --git a/src/os_string_builder.rs b/src/os_string_builder.rs index a701f6f..0879e2c 100644 --- a/src/os_string_builder.rs +++ b/src/os_string_builder.rs @@ -98,6 +98,9 @@ macro_rules! osf { use $crate::os_string_builder::OsStringBuilder; OsStringBuilder::default() }}; + ($s:literal $(,$arg:tt)*) => { + osf!() + format!($s, $($arg)*) + }; ($s:expr) => { osf!() + $s }; @@ -110,6 +113,10 @@ mod test_builder { #[test] fn test_build() { assert_eq!(osf!("foo") + "Bar", "fooBar"); + assert_eq!(osf!(PathBuf::from("foo")) + "Bar", "fooBar"); + let o = 'o'; + assert_eq!(osf!("fo{o}") + "Bar", "fooBar"); + assert_eq!(osf!("fo{}", o) + "Bar", "fooBar"); } #[test] diff --git a/src/server.rs b/src/server.rs index 75772dc..b6a8a43 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,3 +1,4 @@ +use crate::get_home_directory; use std::cell::LazyCell; use std::error::Error; use std::fmt::{Display, Formatter}; @@ -9,14 +10,14 @@ use std::str::FromStr; #[derive(Debug, Clone)] pub enum ServerReference { Resolved(Server), - Name(String), + Identifier(String), } impl ServerReference { - pub fn get_name(&self) -> &str { + pub fn get_identifier(&self) -> &str { match self { - ServerReference::Resolved(server) => &server.ssh_name, - ServerReference::Name(name) => name, + ServerReference::Resolved(server) => server.address.identifier(), + ServerReference::Identifier(id) => id, } } @@ -24,7 +25,7 @@ impl ServerReference { pub fn resolve(self, configured_servers: &[Server]) -> Option { match self { ServerReference::Resolved(server) => Some(server), - ServerReference::Name(name) => Self::resolve_server_name(&name, configured_servers), + ServerReference::Identifier(name) => Self::resolve_server_name(&name, configured_servers), } } @@ -36,7 +37,7 @@ impl ServerReference { { match self { ServerReference::Resolved(server) => Some(server), - ServerReference::Name(name) => Self::resolve_server_name(&name, provider), + ServerReference::Identifier(name) => Self::resolve_server_name(&name, provider), } } @@ -51,17 +52,17 @@ impl ServerReference { { match self { ServerReference::Resolved(server) => Ok(Some(server)), - ServerReference::Name(name) => provider + ServerReference::Identifier(name) => provider .as_ref() .map_err(|e| e.clone()) .map(|servers| Self::resolve_server_name(&name, servers)), } } - fn resolve_server_name(name: &str, servers: &[Server]) -> Option { + fn resolve_server_name(identifier: &str, servers: &[Server]) -> Option { servers .iter() - .find(|server| server.ssh_name == name) + .find(|server| server.address.identifier() == identifier) .cloned() } } @@ -72,13 +73,13 @@ impl FromStr for ServerReference { fn from_str(s: &str) -> Result { Server::from_str(s) .map(Self::Resolved) - .or_else(|_| Ok(Self::Name(s.to_string()))) + .or_else(|_| Ok(Self::Identifier(s.to_string()))) } } impl PartialEq for ServerReference { fn eq(&self, other: &Self) -> bool { - self.get_name() == other.get_name() + self.get_identifier() == other.get_identifier() } } @@ -86,7 +87,7 @@ impl Eq for ServerReference {} impl Hash for ServerReference { fn hash(&self, state: &mut H) { - self.get_name().hash(state); + self.get_identifier().hash(state); } } @@ -94,7 +95,7 @@ impl Display for ServerReference { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ServerReference::Resolved(server) => write!(f, "{}", server), - ServerReference::Name(name) => write!(f, "{}", name), + ServerReference::Identifier(name) => write!(f, "{}", name), } } } @@ -112,32 +113,95 @@ impl Error for ServerReferenceParseError {} #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Server { - pub ssh_name: String, + pub address: ServerAddress, pub server_directory_path: PathBuf, } +impl Server { + pub fn get_name(&self) -> &str { + match &self.address { + ServerAddress::Ssh { ssh_address } => ssh_address, + ServerAddress::Localhost => "this computer", + } + } +} + impl FromStr for Server { type Err = ServerParseError; fn from_str(s: &str) -> Result { s.split_once(':') .ok_or(ServerParseError::MissingServerDirectory) - .map(|(name, directory)| Self { - ssh_name: name.to_string(), - server_directory_path: PathBuf::from(directory), + .and_then(|(identifier, server_directory)| { + let address = ServerAddress::from_str(identifier); + let mut server_directory_path = PathBuf::from(server_directory); + if let ServerAddress::Localhost = &address { + let home_directory = get_home_directory() + .map_err(|e| ServerParseError::HomeDirectoryRequired { detail_message: e })?; + server_directory_path = home_directory.join(&server_directory_path); + } + Ok(Self { + address, + server_directory_path, + }) }) } } impl Display for Server { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.ssh_name, self.server_directory_path.to_string_lossy()) + write!( + f, + "{}{}", + match &self.address { + ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"), + ServerAddress::Localhost => "".to_string(), + }, + self.server_directory_path.to_string_lossy() + ) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum ServerAddress { + Ssh { ssh_address: String }, + Localhost, +} + +impl ServerAddress { + pub fn ssh(ssh_address: S) -> Self + where + S: ToString, + { + Self::Ssh { + ssh_address: ssh_address.to_string(), + } + } + + pub fn from_str(s: S) -> Self + where + S: ToString, + { + let s = s.to_string(); + if s == "." { + Self::Localhost + } else { + Self::ssh(s) + } + } + + pub fn identifier(&self) -> &str { + match self { + ServerAddress::Ssh { ssh_address } => ssh_address, + ServerAddress::Localhost => ".", + } } } #[derive(Debug)] pub enum ServerParseError { MissingServerDirectory, + HomeDirectoryRequired { detail_message: String }, } impl Display for ServerParseError { @@ -150,6 +214,10 @@ impl Display for ServerParseError { double colon to point to the home directory, e.g: 'lobby:'" ) } + ServerParseError::HomeDirectoryRequired { detail_message } => write!( + f, + "localhost requires home directory, but: {detail_message}" + ), } } } @@ -158,23 +226,24 @@ impl Error for ServerParseError {} #[cfg(test)] mod test_server_reference { - use crate::server::{Server, ServerReference}; + use crate::server::{Server, ServerAddress, ServerReference}; use std::path::PathBuf; use std::str::FromStr; #[test] fn test_from_str() { assert_eq!( - ServerReference::Name("foo".to_string()), + ServerReference::Identifier("foo".to_string()), ServerReference::from_str("foo").unwrap() ); assert_eq!( ServerReference::Resolved(Server { - ssh_name: "crea".to_string(), + address: ServerAddress::Ssh { + ssh_address: "crea".to_string() + }, server_directory_path: PathBuf::from("server/creative2") }), ServerReference::from_str("crea:server/creative2").unwrap() ); } } -