Allow localhost as server target

This commit is contained in:
Leonard Steppy 2025-02-02 02:23:25 +01:00
parent a83cf1013c
commit b6b389e5a0
7 changed files with 230 additions and 84 deletions

View File

@ -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 <file>" #<file> 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`.

View File

@ -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)?;
}

View File

@ -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)?;

View File

@ -7,14 +7,20 @@ pub struct Logger {
macro_rules! define_log_function {
($name:ident, $level:ident) => {
pub fn $name<S>(&self, message: S) where S: ToString {
self.log(LogLevel::$level, message.to_string());
}
pub fn $name<S>(&self, message: S)
where
S: ToString,
{
self.log(LogLevel::$level, message.to_string());
}
};
}
impl Logger {
pub fn log<S>(&self, level: LogLevel, message: S) where S: ToString {
pub fn log<S>(&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)]

View File

@ -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::<Vec<_>>();
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<PathBuf, String> {
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<Vec<Server>, 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");
}
}

View File

@ -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]

View File

@ -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<Server> {
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<Server> {
fn resolve_server_name(identifier: &str, servers: &[Server]) -> Option<Server> {
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<Self, Self::Err> {
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<H: Hasher>(&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<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),
.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<S>(ssh_address: S) -> Self
where
S: ToString,
{
Self::Ssh {
ssh_address: ssh_address.to_string(),
}
}
pub fn from_str<S>(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()
);
}
}