2024-12-11 14:13:32 +01:00
|
|
|
use clap::{Parser, Subcommand, ValueEnum};
|
2024-12-11 10:42:44 +01:00
|
|
|
use std::env;
|
2024-12-11 16:37:15 +01:00
|
|
|
use std::error::Error;
|
|
|
|
|
use std::fmt::{Display, Formatter};
|
2024-12-11 10:42:44 +01:00
|
|
|
use std::path::PathBuf;
|
2024-12-11 16:37:15 +01:00
|
|
|
use std::str::FromStr;
|
2024-12-11 10:42:44 +01:00
|
|
|
|
2024-12-11 14:13:32 +01:00
|
|
|
const SERVERS_ENV_VAR: &str = "MSSH_SERVERS";
|
2024-12-11 10:42:44 +01:00
|
|
|
|
2024-12-11 14:13:32 +01:00
|
|
|
/// Uploads a file or executes a command on multiple configured servers
|
2024-12-11 10:42:44 +01:00
|
|
|
///
|
2024-12-11 14:13:32 +01:00
|
|
|
/// Servers must either be configured via environment variable or denote their server directory with
|
|
|
|
|
/// a double colon: crea:home/crea.
|
2024-12-11 10:42:44 +01:00
|
|
|
///
|
2024-12-11 14:13:32 +01:00
|
|
|
/// --- Configuration via environment variable ---
|
2024-12-11 10:42:44 +01:00
|
|
|
///
|
2024-12-11 14:13:32 +01:00
|
|
|
/// Use MSSH_SERVERS="crea:home/crea,sky:sky,lobby:,city:city2" to configure servers.
|
2024-12-11 10:42:44 +01:00
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
|
#[command(version, about, long_about)]
|
|
|
|
|
struct Args {
|
2024-12-11 14:13:32 +01:00
|
|
|
/// The action to perform
|
|
|
|
|
#[command(subcommand)]
|
|
|
|
|
command: Command,
|
|
|
|
|
/// The ssh names and optionally home directories of the servers to perform the action on
|
2024-12-11 16:37:15 +01:00
|
|
|
#[arg(num_args = 1.., value_parser = ServerReference::from_str)]
|
|
|
|
|
servers: Vec<ServerReference>,
|
2024-12-11 14:13:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
},
|
|
|
|
|
/// 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,
|
2024-12-11 10:42:44 +01:00
|
|
|
}
|
|
|
|
|
|
2024-12-11 16:37:15 +01:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
enum ServerReference {
|
|
|
|
|
Resolved(Server),
|
|
|
|
|
Name(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ServerReference {
|
|
|
|
|
//TODO lazy resolve method which gets a provider for configured servers
|
|
|
|
|
pub fn resolve(self, configured_servers: &[Server]) -> Option<Server> {
|
|
|
|
|
match self {
|
|
|
|
|
ServerReference::Resolved(server) => Some(server),
|
|
|
|
|
ServerReference::Name(name) => configured_servers
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|server| server.ssh_name == name)
|
|
|
|
|
.cloned(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl FromStr for ServerReference {
|
|
|
|
|
type Err = ServerParseError;
|
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
Server::from_str(s)
|
|
|
|
|
.map(Self::Resolved)
|
|
|
|
|
.or_else(|_| Ok(Self::Name(s.to_string())))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 10:42:44 +01:00
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
|
|
|
struct Server {
|
|
|
|
|
pub ssh_name: String,
|
|
|
|
|
pub server_directory_path: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-11 16:37:15 +01:00
|
|
|
impl FromStr for Server {
|
|
|
|
|
type Err = ServerParseError;
|
|
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
|
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 {}
|
|
|
|
|
|
2024-12-11 10:42:44 +01:00
|
|
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
|
|
|
struct PluginInfo {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub version: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() -> Result<(), String> {
|
|
|
|
|
let args = Args::parse();
|
|
|
|
|
dbg!(&args);
|
|
|
|
|
|
|
|
|
|
let configured_servers = parse_server_configuration_from_env()?;
|
|
|
|
|
dbg!(&configured_servers);
|
2024-12-11 14:13:32 +01:00
|
|
|
|
2024-12-11 10:42:44 +01:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_server_configuration_from_env() -> Result<Vec<Server>, 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<Vec<Server>, String> {
|
2024-12-11 16:37:15 +01:00
|
|
|
config_str
|
|
|
|
|
.split(',')
|
|
|
|
|
.map(|server_entry| {
|
|
|
|
|
Server::from_str(server_entry)
|
|
|
|
|
.map_err(|e| format!("Invalid server entry '{server_entry}': {e}"))
|
2024-12-11 10:42:44 +01:00
|
|
|
})
|
2024-12-11 16:37:15 +01:00
|
|
|
.collect()
|
2024-12-11 10:42:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod test {
|
|
|
|
|
use crate::{parse_server_configuration, Server};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_parse_server_configuration() {
|
2024-12-11 14:13:32 +01:00
|
|
|
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
|
|
|
|
|
);
|
2024-12-11 10:42:44 +01:00
|
|
|
}
|
|
|
|
|
}
|