Add editor feature
This commit is contained in:
parent
fbf1c1ca76
commit
45914bd567
@ -6,3 +6,4 @@ edition = "2021"
|
||||
[dependencies]
|
||||
clap = { version = "4.5.23", features = ["derive"] }
|
||||
lazy-regex = "3.3.0"
|
||||
shell-words = "1.1.0"
|
||||
|
||||
15
README.md
15
README.md
@ -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).
|
||||
|
||||
102
src/main.rs
102
src/main.rs
@ -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(())
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user