Add editor feature

This commit is contained in:
Leonard Steppy 2024-12-15 14:34:19 +01:00
parent fbf1c1ca76
commit 45914bd567
4 changed files with 144 additions and 8 deletions

View File

@ -6,3 +6,4 @@ edition = "2021"
[dependencies]
clap = { version = "4.5.23", features = ["derive"] }
lazy-regex = "3.3.0"
shell-words = "1.1.0"

View File

@ -1,10 +1,11 @@
# multi-ssh
An application to upload a file or perform a command on multiple servers via ssh and scp.
An application to upload or edit a file or perform a command on multiple servers via ssh and scp.
## Configuration
Set the environment variable `MSSH_SERVERS` to configure the server directories of your ssh servers.
Use the `MSSH_EDITOR` variable to configure your editor command.
Furthermore, ensure that in your shell the following commands are set up and working:
- ssh
- scp
@ -13,10 +14,11 @@ Furthermore, ensure that in your shell the following commands are set up and wor
Ergo you should be able to connect to your desired servers via ssh and be able to start an ssh agent.
### Linux example
### Environment variable setup example for linux
```bash
export MSSH_SERVERS="crea:server/creative2,sky:sky,city:city2"
export MSSH_EDITOR="kate -b <file>" #<file> is the placeholder for the file name
```
## Usage
@ -58,6 +60,15 @@ For detailed usage of the upload feature see
multi-ssh -u --help
```
### Editor example
Edit the server properties of steptech
```bash
multi-ssh steptech -e server.properties
```
Note that the above command will create a `.mssh` folder in your current working directory. That directory can be
changed with the `-d` flag.
## Building from source
This is a rust project so you will need [rust and cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html).

View File

@ -5,19 +5,23 @@ mod server;
use crate::action::{Action, FileAction, ServerActions};
use crate::file::{FileMatcher, FileNameInfo};
use crate::os_string_builder::ReplaceWithOsStr;
use clap::{Parser, Subcommand, ValueEnum};
use lazy_regex::{lazy_regex, Lazy, Regex};
use server::{Server, ServerReference};
use std::cell::LazyCell;
use std::env;
use std::hash::Hash;
use std::io::Write;
use std::iter::once;
use std::path::PathBuf;
use std::process::Stdio;
use std::str::FromStr;
use std::{env, fs};
const SERVERS_ENV_VAR: &str = "MSSH_SERVERS";
const EDITOR_ENV_VAR: &str = "MSSH_EDITOR";
const FILE_PLACEHOLDER: &str = "<file>";
type ShellCmd = std::process::Command;
/// Uploads a file or executes a command on multiple configured servers
@ -27,7 +31,7 @@ type ShellCmd = std::process::Command;
///
/// --- Configuration via environment variable ---
///
/// Use MSSH_SERVERS="crea:home/crea,sky:sky,lobby:,city:city2" to configure servers.
/// Use `MSSH_SERVERS="crea:home/crea,sky:sky,lobby:,city:city2"` to configure servers.
#[derive(Parser, Debug)]
#[command(version, about, long_about)]
struct Args {
@ -61,10 +65,35 @@ enum Command {
},
/// Execute a command on the servers
#[command(visible_short_flag_alias = 'c')]
#[allow(clippy::enum_variant_names)]
Command {
/// The command to execute
command: String,
},
/// Open a file in your local editor and upload it afterward.
///
/// The editor will be opened for every server in the order provided by you.
/// The command to start the editor can be configured via environment variable `MSSH_EDITOR`:
///
/// `export MSSH_EDITOR="nano <file>"`
///
/// File is a placeholder that will be replaced with the actual name of the file.
#[command(visible_short_flag_alias = 'e')]
Editor {
/// A path to the file to edit, relative to the server directory
file: PathBuf,
/// The command to start the editor. Supports the placeholder `<file>`, e.g. "nano <file>".
///
/// If omitted, the command will be taken from the environment variable `MSSH_EDITOR`.
#[arg(short, long)]
editor: Option<String>,
/// The directory where to save the file to
#[arg(short = 'd', long, default_value = ".mssh/downloads")]
working_directory: PathBuf,
/// Override existing files in the working directory
#[arg(short = 'f', long = "override", default_value = "false")]
override_existing: bool,
},
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default, ValueEnum)]
@ -107,12 +136,11 @@ fn main() -> Result<(), String> {
no_confirm,
file_name,
} => {
if servers.is_empty() {
println!("Please provide some servers to upload to. See --help");
return Ok(())
return Ok(());
}
start_ssh_agent()?;
let file_name_info =
@ -278,6 +306,70 @@ fn main() -> Result<(), String> {
}
println!("Done!");
}
Command::Editor {
file,
editor,
working_directory,
override_existing,
} => {
//get editor
let editor = editor.ok_or(()).or_else(|_| env::var(EDITOR_ENV_VAR).map_err(|e| format!("You have not specified an editor. Please do so using the --editor flag or the {EDITOR_ENV_VAR} environment variable: {e}")))?;
fs::create_dir_all(&working_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
let file_name = file
.file_name()
.ok_or("can only edit files, not directories")?;
if !override_existing
&& fs::read_dir(&working_directory)
.map_err(|e| format!("failed to open working directory: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("error while querying working directory contents: {e}"))?
.iter()
.any(|entry| entry.file_name() == file_name)
{
return Err(format!(
"A file with the name {} already exists in {}. You can override it with --override",
file_name.to_string_lossy(),
working_directory.to_string_lossy()
));
}
start_ssh_agent()?;
for server in servers {
println!("Downloading file from {}...", server.ssh_name);
ShellCmd::new("scp")
.arg(osf!(&server.ssh_name) + ":" + server.server_directory_path.join(&file))
.arg(&working_directory)
.status()
.map_err(|e| format!("failed to download file: {e}"))?;
//open file in editor
let mut shell_args = shell_words::split(&editor)
.map_err(|e| format!("failed to parse editor command: {e}"))?
.into_iter()
.map(|part| part.replace_with_os_str(FILE_PLACEHOLDER, working_directory.join(file_name)))
.collect::<Vec<_>>();
let command = shell_args.remove(0);
ShellCmd::new(command)
.args(shell_args)
.status()
.map_err(|e| format!("failed to open file in editor: {e}"))?;
//upload file again
ShellCmd::new("scp")
.arg(working_directory.join(file_name))
.arg(osf!(&server.ssh_name) + ":" + server.server_directory_path.join(&file))
.status()
.map_err(|e| format!("failed to upload file again: {e}"))?;
}
println!("Done!");
}
}
Ok(())

View File

@ -3,6 +3,24 @@ use std::fmt::{Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign};
pub trait ReplaceWithOsStr<'a, Pattern = &'a str> {
#[must_use]
fn replace_with_os_str(&'a self, pattern: Pattern, replacement: impl AsRef<OsStr>) -> OsString;
}
impl<'a> ReplaceWithOsStr<'a> for str {
fn replace_with_os_str(&'a self, pattern: &'a str, replacement: impl AsRef<OsStr>) -> OsString {
let mut parts = self.split(pattern).collect::<Vec<_>>();
let mut builder = OsStringBuilder::from(parts.remove(0));
let replacement = replacement.as_ref();
for part in parts {
builder += replacement;
builder += part;
}
builder.build()
}
}
#[derive(Clone, Default, Eq)]
pub struct OsStringBuilder {
result: OsString,
@ -86,7 +104,7 @@ macro_rules! osf {
}
#[cfg(test)]
mod test {
mod test_builder {
use std::path::PathBuf;
#[test]
@ -102,3 +120,17 @@ mod test {
assert_eq!(osf!("cd ") + foo.join(&bar), "cd foo/bar");
}
}
#[cfg(test)]
mod test_replace_with_os_str {
use crate::os_string_builder::ReplaceWithOsStr;
use std::ffi::OsString;
use std::path::PathBuf;
#[test]
fn test_replace() {
let file = PathBuf::from("foo.txt");
assert_eq!(OsString::from("nano foo.txt"), "nano <file>".replace_with_os_str("<file>", file));
}
}