Add editor feature
This commit is contained in:
parent
fbf1c1ca76
commit
45914bd567
@ -6,3 +6,4 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.23", features = ["derive"] }
|
clap = { version = "4.5.23", features = ["derive"] }
|
||||||
lazy-regex = "3.3.0"
|
lazy-regex = "3.3.0"
|
||||||
|
shell-words = "1.1.0"
|
||||||
|
|||||||
15
README.md
15
README.md
@ -1,10 +1,11 @@
|
|||||||
# multi-ssh
|
# 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
|
## Configuration
|
||||||
|
|
||||||
Set the environment variable `MSSH_SERVERS` to configure the server directories of your ssh servers.
|
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:
|
Furthermore, ensure that in your shell the following commands are set up and working:
|
||||||
- ssh
|
- ssh
|
||||||
- scp
|
- 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.
|
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
|
```bash
|
||||||
export MSSH_SERVERS="crea:server/creative2,sky:sky,city:city2"
|
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
|
## Usage
|
||||||
@ -58,6 +60,15 @@ For detailed usage of the upload feature see
|
|||||||
multi-ssh -u --help
|
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
|
## 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).
|
This is a rust project so you will need [rust and cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html).
|
||||||
|
|||||||
102
src/main.rs
102
src/main.rs
@ -5,19 +5,23 @@ mod server;
|
|||||||
|
|
||||||
use crate::action::{Action, FileAction, ServerActions};
|
use crate::action::{Action, FileAction, ServerActions};
|
||||||
use crate::file::{FileMatcher, FileNameInfo};
|
use crate::file::{FileMatcher, FileNameInfo};
|
||||||
|
use crate::os_string_builder::ReplaceWithOsStr;
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
use lazy_regex::{lazy_regex, Lazy, Regex};
|
use lazy_regex::{lazy_regex, Lazy, Regex};
|
||||||
use server::{Server, ServerReference};
|
use server::{Server, ServerReference};
|
||||||
use std::cell::LazyCell;
|
use std::cell::LazyCell;
|
||||||
use std::env;
|
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
const SERVERS_ENV_VAR: &str = "MSSH_SERVERS";
|
const SERVERS_ENV_VAR: &str = "MSSH_SERVERS";
|
||||||
|
const EDITOR_ENV_VAR: &str = "MSSH_EDITOR";
|
||||||
|
const FILE_PLACEHOLDER: &str = "<file>";
|
||||||
|
|
||||||
type ShellCmd = std::process::Command;
|
type ShellCmd = std::process::Command;
|
||||||
|
|
||||||
/// Uploads a file or executes a command on multiple configured servers
|
/// Uploads a file or executes a command on multiple configured servers
|
||||||
@ -27,7 +31,7 @@ type ShellCmd = std::process::Command;
|
|||||||
///
|
///
|
||||||
/// --- Configuration via environment variable ---
|
/// --- 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)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about)]
|
#[command(version, about, long_about)]
|
||||||
struct Args {
|
struct Args {
|
||||||
@ -61,10 +65,35 @@ enum Command {
|
|||||||
},
|
},
|
||||||
/// Execute a command on the servers
|
/// Execute a command on the servers
|
||||||
#[command(visible_short_flag_alias = 'c')]
|
#[command(visible_short_flag_alias = 'c')]
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
Command {
|
Command {
|
||||||
/// The command to execute
|
/// The command to execute
|
||||||
command: String,
|
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)]
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default, ValueEnum)]
|
||||||
@ -107,12 +136,11 @@ fn main() -> Result<(), String> {
|
|||||||
no_confirm,
|
no_confirm,
|
||||||
file_name,
|
file_name,
|
||||||
} => {
|
} => {
|
||||||
|
|
||||||
if servers.is_empty() {
|
if servers.is_empty() {
|
||||||
println!("Please provide some servers to upload to. See --help");
|
println!("Please provide some servers to upload to. See --help");
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
start_ssh_agent()?;
|
start_ssh_agent()?;
|
||||||
|
|
||||||
let file_name_info =
|
let file_name_info =
|
||||||
@ -278,6 +306,70 @@ fn main() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
println!("Done!");
|
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(())
|
Ok(())
|
||||||
|
|||||||
@ -3,6 +3,24 @@ use std::fmt::{Debug, Formatter};
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::ops::{Add, AddAssign};
|
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)]
|
#[derive(Clone, Default, Eq)]
|
||||||
pub struct OsStringBuilder {
|
pub struct OsStringBuilder {
|
||||||
result: OsString,
|
result: OsString,
|
||||||
@ -86,7 +104,7 @@ macro_rules! osf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test_builder {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -102,3 +120,17 @@ mod test {
|
|||||||
assert_eq!(osf!("cd ") + foo.join(&bar), "cd foo/bar");
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user