use clap::{Parser, Subcommand, ValueEnum}; use std::cell::LazyCell; use std::env; use std::error::Error; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::path::PathBuf; use std::str::FromStr; const SERVERS_ENV_VAR: &str = "MSSH_SERVERS"; /// Uploads a file or executes a command on multiple configured servers /// /// Servers must either be configured via environment variable or denote their server directory with /// a double colon: crea:home/crea. /// /// --- Configuration via environment variable --- /// /// Use MSSH_SERVERS="crea:home/crea,sky:sky,lobby:,city:city2" to configure servers. #[derive(Parser, Debug)] #[command(version, about, long_about)] struct Args { /// The action to perform #[command(subcommand)] command: Command, /// The ssh names and optionally home directories of the servers to perform the action on #[arg(num_args = 1.., value_parser = ServerReference::from_str)] servers: Vec, } #[derive(Subcommand, Debug)] enum Command { /// Upload a file to the servers #[command(visible_short_flag_alias = 'u')] Upload { /// The file to upload file: PathBuf, /// How to handle older versions of the file #[arg(short = 'a', long, default_value = "delete", default_missing_value = "archive", num_args = 0..1)] old_version_policy: OldVersionPolicy, /// The directory where to upload to #[arg(short = 'p', long, default_value = "plugins")] upload_directory: PathBuf, }, /// Execute a command on the servers #[command(visible_short_flag_alias = 'c')] Command { /// The command to execute command: String, }, } #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default, ValueEnum)] enum OldVersionPolicy { /// Ignore the existence of older versions Ignore, /// Rename older versions: foo.jar -> foo.jarr Archive, /// Delete older versions #[default] Delete, } #[derive(Debug, Clone)] enum ServerReference { Resolved(Server), Name(String), } impl ServerReference { pub fn get_name(&self) -> &str { match self { ServerReference::Resolved(server) => &server.ssh_name, ServerReference::Name(name) => name, } } #[allow(dead_code)] 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), } } #[allow(dead_code)] pub fn resolve_lazy(self, provider: &mut LazyCell) -> Option where S: Deref, F: FnOnce() -> S, { match self { ServerReference::Resolved(server) => Some(server), ServerReference::Name(name) => Self::resolve_server_name(&name, provider), } } pub fn try_resolve_lazy( self, provider: &mut LazyCell, F>, ) -> Result, E> where S: Deref, F: FnOnce() -> Result, E: Clone, { match self { ServerReference::Resolved(server) => Ok(Some(server)), ServerReference::Name(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 { servers .iter() .find(|server| server.ssh_name == name) .cloned() } } impl FromStr for ServerReference { type Err = ServerReferenceParseError; fn from_str(s: &str) -> Result { Server::from_str(s) .map(Self::Resolved) .or_else(|_| Ok(Self::Name(s.to_string()))) } } impl PartialEq for ServerReference { fn eq(&self, other: &Self) -> bool { self.get_name() == other.get_name() } } impl Eq for ServerReference {} impl Hash for ServerReference { fn hash(&self, state: &mut H) { self.get_name().hash(state); } } 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), } } } #[derive(Debug)] enum ServerReferenceParseError {} impl Display for ServerReferenceParseError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) //replace that with an actual implementation if there ever are any variants } } impl Error for ServerReferenceParseError {} #[derive(Debug, Clone, Eq, PartialEq, Hash)] struct Server { pub ssh_name: String, pub server_directory_path: PathBuf, } 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), }) } } impl Display for Server { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{:?}", self.ssh_name, self.server_directory_path) } } #[derive(Debug)] enum ServerParseError { MissingServerDirectory, } impl Display for ServerParseError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ServerParseError::MissingServerDirectory => { write!( f, "String is not specifying a server directory. Please use an empty string after a \ double colon to point to the home directory, e.g: 'lobby:'" ) } } } } impl Error for ServerParseError {} #[derive(Debug, Clone, Eq, PartialEq, Hash)] struct PluginInfo { pub name: String, pub version: Option, } fn main() -> Result<(), String> { let args = Args::parse(); dbg!(&args); let mut configured_servers = LazyCell::new(parse_server_configuration_from_env); let servers = args .servers .iter() .map(|server_reference| { let server_name = server_reference.get_name(); server_reference .clone() .try_resolve_lazy(&mut configured_servers) .map_err(|msg| format!("Can't resolve server directory for '{server_name}': {msg}")) .and_then(|opt_server| { opt_server.ok_or(format!( "no server directory has been configured for server '{server_name}'" )) }) }) .collect::, _>>()?; dbg!(&servers); Ok(()) } fn parse_server_configuration_from_env() -> Result, String> { env::var(SERVERS_ENV_VAR) .map_err(|_| format!("Missing environment variable {}", SERVERS_ENV_VAR)) .and_then(|value| parse_server_configuration(&value)) } fn parse_server_configuration(config_str: &str) -> Result, String> { config_str .split(',') .map(|server_entry| { Server::from_str(server_entry) .map_err(|e| format!("Invalid server entry '{server_entry}': {e}")) }) .collect() } #[cfg(test)] mod test { use crate::{parse_server_configuration, Server, ServerReference}; use std::path::PathBuf; use std::str::FromStr; #[test] fn test_parse_server_configuration() { let servers = parse_server_configuration("foo:bar,fizz:buzz/bizz").expect("valid server configuration"); assert_eq!( vec![ Server { ssh_name: "foo".to_string(), server_directory_path: PathBuf::from("bar"), }, Server { ssh_name: "fizz".to_string(), server_directory_path: PathBuf::from("buzz/bizz"), } ], servers ); } #[test] fn test_server_reference_from_str() { assert_eq!( ServerReference::Name("foo".to_string()), ServerReference::from_str("foo").unwrap() ); assert_eq!( ServerReference::Resolved(Server { ssh_name: "crea".to_string(), server_directory_path: PathBuf::from("server/creative2") }), ServerReference::from_str("crea:server/creative2").unwrap() ); } }