WIP: 26-integration-tests #35
@ -84,7 +84,3 @@ Once you have that installed, just run
|
|||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
and you will find an executable in `target/release`.
|
and you will find an executable in `target/release`.
|
||||||
|
|
||||||
### Unit tests
|
|
||||||
|
|
||||||
In order for the unit tests to pass, you will need `python3`.
|
|
||||||
|
|||||||
200
src/command.rs
200
src/command.rs
@ -1,200 +0,0 @@
|
|||||||
use crate::log;
|
|
||||||
use crate::logger::{LogLevel, Logger};
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
|
||||||
use std::io;
|
|
||||||
use std::iter::once;
|
|
||||||
use std::process::{Command, ExitStatus, Output};
|
|
||||||
|
|
||||||
pub trait LogRunnable {
|
|
||||||
fn run(&mut self, logger: &Logger) -> Result<(), CommandSpecificError<ExecutionError>>;
|
|
||||||
fn collect_output(&mut self) -> Result<Output, CommandSpecificError<ExecutionError>>;
|
|
||||||
fn collect_full_output(&mut self) -> Result<Output, CommandSpecificError<StartError>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogRunnable for Command {
|
|
||||||
fn run(&mut self, logger: &Logger) -> Result<(), CommandSpecificError<ExecutionError>> {
|
|
||||||
run(self, logger).map_err(|error| CommandSpecificError {
|
|
||||||
command: self,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_output(&mut self) -> Result<Output, CommandSpecificError<ExecutionError>> {
|
|
||||||
collect_output(self, None).map_err(|error| CommandSpecificError {
|
|
||||||
command: self,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_full_output(&mut self) -> Result<Output, CommandSpecificError<StartError>> {
|
|
||||||
collect_full_output(self).map_err(|error| CommandSpecificError {
|
|
||||||
command: self,
|
|
||||||
error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(command: &mut Command, logger: &Logger) -> Result<(), ExecutionError> {
|
|
||||||
match logger.level {
|
|
||||||
LogLevel::Debug | LogLevel::Info => {
|
|
||||||
let status = command.status()?;
|
|
||||||
if !status.success() {
|
|
||||||
Err(status)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LogLevel::Error => {
|
|
||||||
collect_output(command, Some(logger))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_output(
|
|
||||||
command: &mut Command,
|
|
||||||
logger: Option<&Logger>,
|
|
||||||
) -> Result<Output, ExecutionError> {
|
|
||||||
let output = collect_full_output(command)?;
|
|
||||||
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)?;
|
|
||||||
}
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_full_output(command: &mut Command) -> Result<Output, StartError> {
|
|
||||||
Ok(command.output()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CommandSpecificError<'a, E> {
|
|
||||||
pub command: &'a Command,
|
|
||||||
pub error: E,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> Display for CommandSpecificError<'_, E>
|
|
||||||
where
|
|
||||||
E: Display,
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Failed to execute command '{}': {}",
|
|
||||||
command_to_string(self.command),
|
|
||||||
self.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn command_to_string(command: &Command) -> String {
|
|
||||||
once(command.get_program().to_string_lossy().to_string())
|
|
||||||
.chain(command.get_args().map(|arg| {
|
|
||||||
let arg_str = arg.to_string_lossy();
|
|
||||||
if arg_str.contains(' ') {
|
|
||||||
format!("\"{arg_str}\"")
|
|
||||||
} else {
|
|
||||||
arg_str.to_string()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E> Error for CommandSpecificError<'_, E> where E: Debug + Display {}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct StartError(io::Error);
|
|
||||||
|
|
||||||
impl From<io::Error> for StartError {
|
|
||||||
fn from(value: io::Error) -> Self {
|
|
||||||
StartError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for StartError {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Failed to start command: {}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for StartError {}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ExecutionError {
|
|
||||||
StartError(StartError),
|
|
||||||
BadExitStatus(ExitStatus),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<io::Error> for ExecutionError {
|
|
||||||
fn from(value: io::Error) -> Self {
|
|
||||||
Self::StartError(StartError(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<StartError> for ExecutionError {
|
|
||||||
fn from(value: StartError) -> Self {
|
|
||||||
Self::StartError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ExitStatus> for ExecutionError {
|
|
||||||
fn from(value: ExitStatus) -> Self {
|
|
||||||
Self::BadExitStatus(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ExecutionError {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
ExecutionError::StartError(e) => Display::fmt(e, f),
|
|
||||||
ExecutionError::BadExitStatus(status) => write!(f, "Command failed with {}", status),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for ExecutionError {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use crate::command::{CommandSpecificError, ExecutionError, LogRunnable};
|
|
||||||
use crate::logger::Logger;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_unknown_command() {
|
|
||||||
let mut command = Command::new("python7");
|
|
||||||
let Err(
|
|
||||||
e @ CommandSpecificError {
|
|
||||||
error: ExecutionError::StartError(_),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) = command
|
|
||||||
.args([PathBuf::from("test-ressources/python/exit_1.py")])
|
|
||||||
.run(&Logger::default())
|
|
||||||
else {
|
|
||||||
panic!("command shouldn't exist");
|
|
||||||
};
|
|
||||||
assert_eq!(e.to_string(), "Failed to execute command 'python7': Failed to start command: No such file or directory (os error 2)");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_error() {
|
|
||||||
let mut command = Command::new("python3");
|
|
||||||
let Err(
|
|
||||||
e @ CommandSpecificError {
|
|
||||||
error: ExecutionError::BadExitStatus(_),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
) = command
|
|
||||||
.arg("test-ressources/python/exit_1.py")
|
|
||||||
.run(&Logger::default())
|
|
||||||
else {
|
|
||||||
panic!("command should return exit-code 1")
|
|
||||||
};
|
|
||||||
assert_eq!(e.to_string(), "Failed to execute command 'python3 test-ressources/python/exit_1.py': Command failed with exit status: 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
96
src/environment.rs
Normal file
96
src/environment.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use crate::shell_interface::{
|
||||||
|
build_command_from_shell_command, CommandOutput, CommandResult, ExitStatus, ShellCommand,
|
||||||
|
ShellInterface, StartError,
|
||||||
|
};
|
||||||
|
use std::env::VarError;
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{env, io};
|
||||||
|
|
||||||
|
pub trait Environment {
|
||||||
|
fn args_os(&self) -> Vec<OsString>;
|
||||||
|
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>;
|
||||||
|
fn var<K>(&self, key: K) -> Result<String, VarError>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
self
|
||||||
|
.var_os(key)
|
||||||
|
.ok_or(VarError::NotPresent)
|
||||||
|
.and_then(|s| s.into_string().map_err(VarError::NotUnicode))
|
||||||
|
}
|
||||||
|
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
V: AsRef<OsStr>;
|
||||||
|
fn get_home_directory(&self) -> Option<PathBuf>;
|
||||||
|
fn read_line(&mut self) -> Result<String, io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Prod;
|
||||||
|
|
||||||
|
impl Environment for Prod {
|
||||||
|
fn args_os(&self) -> Vec<OsString> {
|
||||||
|
env::args_os().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>
|
||||||
|
{
|
||||||
|
env::var_os(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn var<K>(&self, key: K) -> Result<String, VarError>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
env::var(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
V: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
env::set_var(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_home_directory(&self) -> Option<PathBuf> {
|
||||||
|
homedir::my_home().ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_line(&mut self) -> Result<String, io::Error> {
|
||||||
|
let mut buffer = String::new();
|
||||||
|
io::stdin().read_line(&mut buffer)?;
|
||||||
|
Ok(buffer.trim().to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellInterface for Prod {
|
||||||
|
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError> {
|
||||||
|
CommandResult {
|
||||||
|
result: build_command_from_shell_command(&command)
|
||||||
|
.status()
|
||||||
|
.map(ExitStatus::from)
|
||||||
|
.map_err(StartError::from),
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_command_output(
|
||||||
|
&mut self,
|
||||||
|
command: ShellCommand,
|
||||||
|
) -> CommandResult<CommandOutput, StartError> {
|
||||||
|
CommandResult {
|
||||||
|
result: build_command_from_shell_command(&command)
|
||||||
|
.output()
|
||||||
|
.map(CommandOutput::from)
|
||||||
|
.map_err(StartError::from),
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/integration_test.rs
Normal file
90
src/integration_test.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
use crate::environment::Environment;
|
||||||
|
use crate::shell_interface::{
|
||||||
|
CommandOutput, CommandResult, ExitStatus, ShellCommand, ShellInterface, StartError,
|
||||||
|
};
|
||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::io::Error;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TestEnvironment {
|
||||||
|
/// passed command line arguments
|
||||||
|
args_os: Vec<OsString>,
|
||||||
|
/// set environment variables - we assume a pure environment by default
|
||||||
|
env_vars: HashMap<OsString, OsString>,
|
||||||
|
/// home directory, relative to the target/test folder
|
||||||
|
home_dir: PathBuf,
|
||||||
|
/// pending lines of std input
|
||||||
|
stdin: VecDeque<String>,
|
||||||
|
/// whether an ssh agent has been started successfully
|
||||||
|
ssh_agent_started: bool,
|
||||||
|
// TODO ssh servers and local server
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SshServer {
|
||||||
|
pub name: String,
|
||||||
|
pub home_dir: FsEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FsEntry {
|
||||||
|
pub name: OsString,
|
||||||
|
pub kind: FsEntryKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FsEntryKind {
|
||||||
|
Directory(Dir),
|
||||||
|
File {
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Dir {
|
||||||
|
pub contents: Vec<FsEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Environment for TestEnvironment {
|
||||||
|
fn args_os(&self) -> Vec<OsString> {
|
||||||
|
self.args_os.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>
|
||||||
|
{
|
||||||
|
self.env_vars.get(key.as_ref()).map(|s| s.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||||
|
where
|
||||||
|
K: AsRef<OsStr>,
|
||||||
|
V: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
self.env_vars.insert(key.as_ref().into(), value.as_ref().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_home_directory(&self) -> Option<PathBuf> {
|
||||||
|
PathBuf::from("target/integration_test").join(&self.home_dir).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_line(&mut self) -> Result<String, Error> {
|
||||||
|
self.stdin.pop_front().ok_or_else(|| Error::other("Unexpected call to read_line: No input prepared"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellInterface for TestEnvironment {
|
||||||
|
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_command_output(
|
||||||
|
&mut self,
|
||||||
|
command: ShellCommand,
|
||||||
|
) -> CommandResult<CommandOutput, StartError> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
465
src/main.rs
465
src/main.rs
@ -1,18 +1,22 @@
|
|||||||
mod action;
|
mod action;
|
||||||
mod command;
|
mod environment;
|
||||||
mod file;
|
mod file;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod integration_test;
|
||||||
mod logger;
|
mod logger;
|
||||||
mod os_string_builder;
|
|
||||||
mod os_str_extension;
|
mod os_str_extension;
|
||||||
|
mod os_string_builder;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod shell_interface;
|
||||||
|
|
||||||
use crate::action::{Action, FileAction, ServerActions};
|
use crate::action::{Action, FileAction, ServerActions};
|
||||||
use crate::command::{CommandSpecificError, ExecutionError, LogRunnable};
|
use crate::environment::{Environment, Prod};
|
||||||
use crate::file::{FileMatcher, FileNameInfo};
|
use crate::file::{FileMatcher, FileNameInfo};
|
||||||
use crate::logger::{LogLevel, Logger};
|
use crate::logger::{LogLevel, Logger};
|
||||||
use crate::os_str_extension::OsStrExtension;
|
use crate::os_str_extension::OsStrExtension;
|
||||||
use crate::os_string_builder::ReplaceWithOsStr;
|
use crate::os_string_builder::ReplaceWithOsStr;
|
||||||
use crate::server::{RelativeLocalPathAnker, ServerAddress};
|
use crate::server::{RelativeLocalPathAnker, ServerAddress};
|
||||||
|
use crate::shell_interface::{ScpParam, ServerCommand, ShellCommand, ShellInterface};
|
||||||
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};
|
||||||
@ -22,15 +26,12 @@ use std::hash::Hash;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{env, fs, io};
|
use std::{env, fs, io};
|
||||||
|
|
||||||
const SERVERS_ENV_VAR: &str = "MSSH_SERVERS";
|
const SERVERS_ENV_VAR: &str = "MSSH_SERVERS";
|
||||||
const EDITOR_ENV_VAR: &str = "MSSH_EDITOR";
|
const EDITOR_ENV_VAR: &str = "MSSH_EDITOR";
|
||||||
const FILE_PLACEHOLDER: &str = "<file>";
|
const FILE_PLACEHOLDER: &str = "<file>";
|
||||||
|
|
||||||
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
|
||||||
///
|
///
|
||||||
/// Servers must either be configured via environment variable or denote their server directory with
|
/// Servers must either be configured via environment variable or denote their server directory with
|
||||||
@ -41,13 +42,13 @@ type ShellCmd = std::process::Command;
|
|||||||
/// 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 {
|
pub struct Args {
|
||||||
/// The action to perform
|
/// The action to perform
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
/// The ssh names and optionally home directories of the servers to perform the action on
|
/// The ssh names and optionally home directories of the servers to perform the action on
|
||||||
#[arg(num_args = 0.., value_parser = ServerReference::from_str)]
|
#[arg(num_args = 0..)]
|
||||||
servers: Vec<ServerReference>,
|
servers: Vec<String>,
|
||||||
/// How verbose logging output should be
|
/// How verbose logging output should be
|
||||||
#[arg(long, default_value = "info", conflicts_with_all = ["quiet", "info"])]
|
#[arg(long, default_value = "info", conflicts_with_all = ["quiet", "info"])]
|
||||||
log_level: LogLevel,
|
log_level: LogLevel,
|
||||||
@ -71,7 +72,7 @@ enum Command {
|
|||||||
/// When this option is set, the file path must be absolute, or relative to the server directory.
|
/// When this option is set, the file path must be absolute, or relative to the server directory.
|
||||||
/// The upload-directory has no influence on where the file will be taken from.
|
/// The upload-directory has no influence on where the file will be taken from.
|
||||||
#[arg(short = 'S', long)]
|
#[arg(short = 'S', long)]
|
||||||
file_server: Option<ServerReference>,
|
file_server: Option<String>,
|
||||||
/// How to handle older versions of the file
|
/// How to handle older versions of the file
|
||||||
#[arg(short = 'a', long, default_value = "delete", default_missing_value = "archive", num_args = 0..=1)]
|
#[arg(short = 'a', long, default_value = "delete", default_missing_value = "archive", num_args = 0..=1)]
|
||||||
old_version_policy: OldVersionPolicy,
|
old_version_policy: OldVersionPolicy,
|
||||||
@ -137,24 +138,26 @@ enum OldVersionPolicy {
|
|||||||
Delete,
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[macro_export]
|
#[derive(Debug, Default)]
|
||||||
macro_rules! input {
|
pub struct Application<E> {
|
||||||
($prompt: tt) => {{
|
pub environment: E,
|
||||||
print!($prompt);
|
|
||||||
io::stdout().flush().expect("failed to flush stdout");
|
|
||||||
let mut buf = String::new();
|
|
||||||
io::stdin()
|
|
||||||
.read_line(&mut buf)
|
|
||||||
.expect("failed to read stdin");
|
|
||||||
buf.trim().to_string()
|
|
||||||
}};
|
|
||||||
() => {
|
|
||||||
input!()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), String> {
|
impl<E> Application<E>
|
||||||
let args = Args::parse();
|
where
|
||||||
|
E: Environment + ShellInterface,
|
||||||
|
{
|
||||||
|
pub fn run(&mut self) -> Result<(), String> {
|
||||||
|
let args = Args::try_parse_from(self.environment.args_os()).map_err(|e| e.to_string())?;
|
||||||
|
self.run_with_args(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_with_args(&mut self, args: Args) -> Result<(), String> {
|
||||||
|
macro_rules! env {
|
||||||
|
() => {
|
||||||
|
&mut self.environment
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let logger = Logger {
|
let logger = Logger {
|
||||||
//all the below options are conflicting with each other so an if else is fine
|
//all the below options are conflicting with each other so an if else is fine
|
||||||
@ -167,10 +170,16 @@ fn main() -> Result<(), String> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut configured_servers = LazyCell::new(parse_server_configuration_from_env);
|
let mut configured_servers = LazyCell::new(|| self.parse_server_configuration_from_env());
|
||||||
let servers = args
|
let servers = args
|
||||||
.servers
|
.servers
|
||||||
.iter()
|
.into_iter()
|
||||||
|
.map(|ref_str| {
|
||||||
|
ServerReference::from_str(&ref_str, || self.get_home_directory())
|
||||||
|
.map_err(|e| format!("Invalid server reference '{ref_str}': {e}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
.map(|server_reference| {
|
.map(|server_reference| {
|
||||||
let server_identifier = server_reference.get_identifier();
|
let server_identifier = server_reference.get_identifier();
|
||||||
server_reference
|
server_reference
|
||||||
@ -196,13 +205,15 @@ fn main() -> Result<(), String> {
|
|||||||
pure,
|
pure,
|
||||||
exclude,
|
exclude,
|
||||||
} => {
|
} => {
|
||||||
require_non_empty_servers(&servers)?;
|
Self::require_non_empty_servers(&servers)?;
|
||||||
require_non_empty(&files, "files to upload")?;
|
Self::require_non_empty(&files, "files to upload")?;
|
||||||
start_ssh_agent(&logger)?;
|
|
||||||
|
|
||||||
//resolve file server
|
//resolve file server
|
||||||
let file_server = match file_server {
|
let file_server = match file_server {
|
||||||
Some(server_reference) => {
|
Some(ref_str) => {
|
||||||
|
let server_reference =
|
||||||
|
ServerReference::from_str(&ref_str, || self.get_home_directory())
|
||||||
|
.map_err(|e| format!("Invalid file-server reference '{ref_str}': {e}"))?;
|
||||||
let file_server_identifier = server_reference.get_identifier().to_string();
|
let file_server_identifier = server_reference.get_identifier().to_string();
|
||||||
let server = server_reference.try_resolve_lazy(&mut configured_servers)
|
let server = server_reference.try_resolve_lazy(&mut configured_servers)
|
||||||
.map_err(|e| format!("Can't resolve server directory for file-server '{file_server_identifier}': {e}"))?
|
.map_err(|e| format!("Can't resolve server directory for file-server '{file_server_identifier}': {e}"))?
|
||||||
@ -212,6 +223,8 @@ fn main() -> Result<(), String> {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.start_ssh_agent(&logger)?;
|
||||||
|
|
||||||
//make sure files exist
|
//make sure files exist
|
||||||
match &file_server {
|
match &file_server {
|
||||||
Some(file_server) => match &file_server.address {
|
Some(file_server) => match &file_server.address {
|
||||||
@ -220,23 +233,29 @@ fn main() -> Result<(), String> {
|
|||||||
files = files
|
files = files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|file| {
|
.map(|file| {
|
||||||
let output = ShellCmd::new("ssh")
|
let output = ShellCommand::Ssh {
|
||||||
.arg(ssh_address)
|
address: ssh_address.to_string(),
|
||||||
.arg(osf!("realpath -e ") + file_server.server_directory_path.join(file))
|
server_command: ServerCommand::Realpath {
|
||||||
.collect_full_output()
|
path: file_server.server_directory_path.join(file),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.in_env(env!())
|
||||||
|
.output()
|
||||||
|
.into_result()
|
||||||
.map_err(|e| format!("Failed to canonicalize files: {e}"))?;
|
.map_err(|e| format!("Failed to canonicalize files: {e}"))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success {
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"Path doesn't match any files on file-server: {}",
|
"Path doesn't match any files on file-server: {}",
|
||||||
file.to_string_lossy()
|
file.to_string_lossy()
|
||||||
))?;
|
))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let denoted_files = osstring_from_ssh_output(output.stdout)
|
let denoted_files = output
|
||||||
|
.stdout
|
||||||
.split(b'\n') //split at line breaks
|
.split(b'\n') //split at line breaks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|bytes| !bytes.is_empty()) //needed since realpath sometimes gives us empty lines
|
.filter(|file_name| !file_name.is_empty()) //needed since realpath sometimes gives us empty lines
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@ -250,9 +269,9 @@ fn main() -> Result<(), String> {
|
|||||||
ServerAddress::Localhost => files
|
ServerAddress::Localhost => files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|file| file_server.server_directory_path.join(file))
|
.map(|file| file_server.server_directory_path.join(file))
|
||||||
.try_for_each(check_local_file_exists)?,
|
.try_for_each(Self::check_local_file_exists)?,
|
||||||
},
|
},
|
||||||
None => files.iter().try_for_each(check_local_file_exists)?,
|
None => files.iter().try_for_each(Self::check_local_file_exists)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_details = files
|
let file_details = files
|
||||||
@ -294,26 +313,32 @@ fn main() -> Result<(), String> {
|
|||||||
server,
|
server,
|
||||||
actions: {
|
actions: {
|
||||||
let present_file_names: Vec<OsString> = match &server.address {
|
let present_file_names: Vec<OsString> = match &server.address {
|
||||||
ServerAddress::Ssh { ssh_address } => osstring_from_ssh_output(
|
ServerAddress::Ssh { ssh_address } => ShellCommand::Ssh {
|
||||||
ShellCmd::new("ssh")
|
address: ssh_address.to_string(),
|
||||||
.arg(ssh_address)
|
server_command: ServerCommand::Ls {
|
||||||
.arg(osf!("ls ") + &working_directory)
|
dir: working_directory.clone(),
|
||||||
.collect_output()
|
},
|
||||||
|
}
|
||||||
|
.in_env(env!())
|
||||||
|
.output()
|
||||||
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
format!(
|
format!(
|
||||||
"Failed to query present files on server {}: {e}",
|
"Failed to query present files on server {}: {e}",
|
||||||
server.get_name()
|
server.get_name()
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.stdout,
|
.stdout
|
||||||
)
|
|
||||||
.split(b'\n')
|
.split(b'\n')
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(OsString::from)
|
.map(OsString::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
ServerAddress::Localhost => fs::read_dir(&working_directory)
|
ServerAddress::Localhost => fs::read_dir(&working_directory)
|
||||||
.map_err(|e| format!("Failed to get files in working directory: {e}"))?
|
.map_err(|e| format!("Failed to get files in working directory: {e}"))?
|
||||||
.map(|entry| entry.map_err(|e| format!("Failed to access directory entry: {e}")))
|
.map(|entry| {
|
||||||
|
entry.map_err(|e| format!("Failed to access directory entry: {e}"))
|
||||||
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
@ -341,7 +366,8 @@ fn main() -> Result<(), String> {
|
|||||||
|
|
||||||
let file_name = file_name_info.to_full_file_name();
|
let file_name = file_name_info.to_full_file_name();
|
||||||
|
|
||||||
let add_action = FileAction::new(file, Action::Add).expect("path points to file");
|
let add_action =
|
||||||
|
FileAction::new(file, Action::Add).expect("path points to file");
|
||||||
|
|
||||||
if pure && present_file_names.iter().any(|file| *file == file_name) {
|
if pure && present_file_names.iter().any(|file| *file == file_name) {
|
||||||
log!(
|
log!(
|
||||||
@ -359,8 +385,7 @@ fn main() -> Result<(), String> {
|
|||||||
if !present_file_names.iter().any(|file| *file == file_name) {
|
if !present_file_names.iter().any(|file| *file == file_name) {
|
||||||
vec![add_action] //file doesn't exist yet
|
vec![add_action] //file doesn't exist yet
|
||||||
} else {
|
} else {
|
||||||
vec![FileAction::new(file, Action::Replace)
|
vec![FileAction::new(file, Action::Replace).expect("path points to file")]
|
||||||
.expect("path points to file")]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OldVersionPolicy::Archive => present_file_names
|
OldVersionPolicy::Archive => present_file_names
|
||||||
@ -392,7 +417,8 @@ fn main() -> Result<(), String> {
|
|||||||
if *present_file == file_name {
|
if *present_file == file_name {
|
||||||
FileAction::new(file, Action::Replace).expect("path points to file")
|
FileAction::new(file, Action::Replace).expect("path points to file")
|
||||||
} else {
|
} else {
|
||||||
FileAction::new(present_file, Action::Delete).expect("path points to file")
|
FileAction::new(present_file, Action::Delete)
|
||||||
|
.expect("path points to file")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@ -427,15 +453,10 @@ fn main() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !no_confirm {
|
if !no_confirm && !self.confirm("Continue?", true) {
|
||||||
match input!("Continue? [Y|n] ").to_lowercase().as_str() {
|
|
||||||
"n" | "no" => {
|
|
||||||
log!(logger, "Aborting...");
|
log!(logger, "Aborting...");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for server_actions in actions {
|
for server_actions in actions {
|
||||||
let server = server_actions.server;
|
let server = server_actions.server;
|
||||||
@ -443,31 +464,36 @@ fn main() -> Result<(), String> {
|
|||||||
for file_action in server_actions.actions {
|
for file_action in server_actions.actions {
|
||||||
match file_action.kind {
|
match file_action.kind {
|
||||||
Action::Add | Action::Replace => {
|
Action::Add | Action::Replace => {
|
||||||
let scp_source = match &file_server {
|
let source = match &file_server {
|
||||||
Some(file_server) => {
|
Some(file_server) => ScpParam::from((
|
||||||
osf!(match &file_server.address {
|
file_server,
|
||||||
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
|
file_server.server_directory_path.join(&file_action.file),
|
||||||
ServerAddress::Localhost => "".to_string(),
|
)),
|
||||||
}) + file_server.server_directory_path.join(&file_action.file)
|
None => ScpParam::from(file_action.file.as_path()),
|
||||||
}
|
|
||||||
None => osf!(&file_action.file),
|
|
||||||
};
|
};
|
||||||
let scp_target = osf!(match &server.address {
|
let destination = ScpParam::from((server, &server_actions.working_directory));
|
||||||
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
|
ShellCommand::Scp {
|
||||||
ServerAddress::Localhost => "".to_string(),
|
source,
|
||||||
}) + &server_actions.working_directory;
|
destination,
|
||||||
ShellCmd::new("scp")
|
}
|
||||||
.arg(scp_source)
|
.in_env(env!())
|
||||||
.arg(scp_target)
|
.run_logged(&logger)
|
||||||
.run(&logger)
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)
|
||||||
.map_err(|e| format!("upload failure: {e}"))?;
|
.map_err(|e| format!("upload failure: {e}"))?;
|
||||||
}
|
}
|
||||||
Action::Delete => match &server.address {
|
Action::Delete => match &server.address {
|
||||||
ServerAddress::Ssh { ssh_address } => {
|
ServerAddress::Ssh { ssh_address } => {
|
||||||
ShellCmd::new("ssh")
|
ShellCommand::Ssh {
|
||||||
.arg(ssh_address)
|
address: ssh_address.to_string(),
|
||||||
.arg(osf!("rm ") + server_actions.working_directory.join(&file_action.file))
|
server_command: ServerCommand::Rm {
|
||||||
.run(&logger)
|
file: server_actions.working_directory.join(&file_action.file),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.in_env(env!())
|
||||||
|
.run_logged(&logger)
|
||||||
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)
|
||||||
.map_err(|e| format!("failed to delete old version: {e}"))?;
|
.map_err(|e| format!("failed to delete old version: {e}"))?;
|
||||||
}
|
}
|
||||||
ServerAddress::Localhost => {
|
ServerAddress::Localhost => {
|
||||||
@ -477,15 +503,17 @@ fn main() -> Result<(), String> {
|
|||||||
},
|
},
|
||||||
Action::Rename { new_name } => match &server.address {
|
Action::Rename { new_name } => match &server.address {
|
||||||
ServerAddress::Ssh { ssh_address } => {
|
ServerAddress::Ssh { ssh_address } => {
|
||||||
ShellCmd::new("ssh")
|
ShellCommand::Ssh {
|
||||||
.arg(ssh_address)
|
address: ssh_address.to_string(),
|
||||||
.arg(
|
server_command: ServerCommand::Mv {
|
||||||
osf!("mv ")
|
source: server_actions.working_directory.join(&file_action.file),
|
||||||
+ server_actions.working_directory.join(&file_action.file)
|
destination: server_actions.working_directory.join(&new_name),
|
||||||
+ " "
|
},
|
||||||
+ server_actions.working_directory.join(&new_name),
|
}
|
||||||
)
|
.in_env(env!())
|
||||||
.run(&logger)
|
.run_logged(&logger)
|
||||||
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)
|
||||||
.map_err(|e| format!("failed to rename: {e}"))?;
|
.map_err(|e| format!("failed to rename: {e}"))?;
|
||||||
}
|
}
|
||||||
ServerAddress::Localhost => {
|
ServerAddress::Localhost => {
|
||||||
@ -501,26 +529,39 @@ fn main() -> Result<(), String> {
|
|||||||
log!(logger, "Done!");
|
log!(logger, "Done!");
|
||||||
}
|
}
|
||||||
Command::Command { command } => {
|
Command::Command { command } => {
|
||||||
start_ssh_agent(&logger)?;
|
self.start_ssh_agent(&logger)?;
|
||||||
require_non_empty_servers(&servers)?;
|
Self::require_non_empty_servers(&servers)?;
|
||||||
for server in servers {
|
for server in servers {
|
||||||
log!(logger, "Running command on '{}'...", server.get_name());
|
log!(logger, "Running command on '{}'...", server.get_name());
|
||||||
match &server.address {
|
match &server.address {
|
||||||
ServerAddress::Ssh { ssh_address } => {
|
ServerAddress::Ssh { ssh_address } => {
|
||||||
ShellCmd::new("ssh")
|
ShellCommand::Ssh {
|
||||||
.arg(ssh_address)
|
address: ssh_address.to_string(),
|
||||||
.arg(osf!("cd ") + server.server_directory_path + "; " + &command)
|
server_command: ServerCommand::Execute {
|
||||||
.run(&logger)
|
working_directory: server.server_directory_path.clone(),
|
||||||
|
command: OsString::from(&command),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.in_env(env!())
|
||||||
|
.run_logged(&logger)
|
||||||
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)
|
||||||
.map_err(|e| format!("{e}"))?;
|
.map_err(|e| format!("{e}"))?;
|
||||||
}
|
}
|
||||||
ServerAddress::Localhost => {
|
ServerAddress::Localhost => {
|
||||||
let mut command_args =
|
let command = shell_words::split(&command)
|
||||||
shell_words::split(&command).map_err(|e| format!("failed to parse command: {e}"))?;
|
.map_err(|e| format!("failed to parse command: {e}"))?
|
||||||
ShellCmd::new(command_args.remove(0))
|
.into_iter()
|
||||||
.args(&command_args)
|
.map(OsString::from)
|
||||||
.current_dir(&server.server_directory_path)
|
.collect();
|
||||||
.run(&logger)
|
ShellCommand::Execute {
|
||||||
.map_err(|e| format!("{e}"))?;
|
working_directory: server.server_directory_path.clone(),
|
||||||
|
command,
|
||||||
|
}
|
||||||
|
.in_env(env!())
|
||||||
|
.run_logged(&logger)
|
||||||
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -536,8 +577,9 @@ fn main() -> Result<(), String> {
|
|||||||
let download_directory = match download_directory {
|
let download_directory = match download_directory {
|
||||||
Some(download_directory) => download_directory,
|
Some(download_directory) => download_directory,
|
||||||
None => {
|
None => {
|
||||||
let home_dir =
|
let home_dir = self
|
||||||
get_home_directory().map_err(|e| format!("Can't determine download directory: {e}"))?;
|
.get_home_directory()
|
||||||
|
.map_err(|e| format!("Missing download-directory: {e}"))?;
|
||||||
home_dir.join("Downloads")
|
home_dir.join("Downloads")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -567,14 +609,13 @@ fn main() -> Result<(), String> {
|
|||||||
download_directory.to_string_lossy()
|
download_directory.to_string_lossy()
|
||||||
);
|
);
|
||||||
|
|
||||||
if !args.quiet {
|
if !args.quiet
|
||||||
match input!("{duplication_notification}. Do you want to replace it? [N|y] ")
|
&& self.confirm(
|
||||||
.to_lowercase()
|
format!("{duplication_notification}. Do you want to replace it?"),
|
||||||
.as_str()
|
false,
|
||||||
|
)
|
||||||
{
|
{
|
||||||
"y" | "yes" => break 'duplicate_check,
|
break 'duplicate_check;
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@ -583,23 +624,24 @@ fn main() -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
require_non_empty_servers(&servers)?;
|
Self::require_non_empty_servers(&servers)?;
|
||||||
start_ssh_agent(&logger)?;
|
self.start_ssh_agent(&logger)?;
|
||||||
|
|
||||||
for server in servers {
|
for server in servers {
|
||||||
log!(logger, "Getting file from {}...", server.get_name());
|
log!(logger, "Getting file from {}...", server.get_name());
|
||||||
let file_source = osf!(match &server.address {
|
let source = ScpParam::from((&server, server.server_directory_path.join(&file)));
|
||||||
ServerAddress::Ssh { ssh_address } => format!("{ssh_address}:"),
|
ShellCommand::Scp {
|
||||||
ServerAddress::Localhost => "".to_string(),
|
source: source.clone(),
|
||||||
}) + server.server_directory_path.join(&file);
|
destination: ScpParam::from(download_directory.as_path()),
|
||||||
ShellCmd::new("scp")
|
}
|
||||||
.arg(&file_source)
|
.in_env(env!())
|
||||||
.arg(&download_directory)
|
.run_logged(&logger)
|
||||||
.run(&logger)
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)
|
||||||
.map_err(|e| format!("download failure: {e}"))?;
|
.map_err(|e| format!("download failure: {e}"))?;
|
||||||
|
|
||||||
//open file in editor
|
//open file in editor
|
||||||
let mut editor_command_args = shell_words::split(&editor)
|
let editor_command = shell_words::split(&editor)
|
||||||
.map_err(|e| format!("failed to parse editor command: {e}"))?
|
.map_err(|e| format!("failed to parse editor command: {e}"))?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|part| {
|
.map(|part| {
|
||||||
@ -607,17 +649,22 @@ fn main() -> Result<(), String> {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let command = editor_command_args.remove(0);
|
ShellCommand::Editor(editor_command)
|
||||||
ShellCmd::new(command)
|
.in_env(env!())
|
||||||
.args(editor_command_args)
|
.run_logged(&logger)
|
||||||
.run(&logger)
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)
|
||||||
.map_err(|e| format!("failed to open file in editor: {e}"))?;
|
.map_err(|e| format!("failed to open file in editor: {e}"))?;
|
||||||
|
|
||||||
//upload file again
|
//upload file again
|
||||||
ShellCmd::new("scp")
|
ShellCommand::Scp {
|
||||||
.arg(download_directory.join(file_name))
|
source: ScpParam::from(download_directory.join(file_name).as_path()),
|
||||||
.arg(&file_source)
|
destination: source,
|
||||||
.run(&logger)
|
}
|
||||||
|
.in_env(env!())
|
||||||
|
.run_logged(&logger)
|
||||||
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(&logger)
|
||||||
.map_err(|e| format!("failed to re-upload file: {e}"))?;
|
.map_err(|e| format!("failed to re-upload file: {e}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -628,20 +675,6 @@ fn main() -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn osstring_from_ssh_output(output: Vec<u8>) -> OsString {
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::ffi::OsStringExt;
|
|
||||||
OsString::from_vec(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
use std::os::windows::ffi::OsStringExt;
|
|
||||||
OsString::from_wide(output.iter().map(|&b| b as u16).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_local_file_exists<P>(path: P) -> Result<(), String>
|
fn check_local_file_exists<P>(path: P) -> Result<(), String>
|
||||||
where
|
where
|
||||||
P: AsRef<Path>,
|
P: AsRef<Path>,
|
||||||
@ -657,45 +690,8 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn check_file_exists_on_server<P, S, D>(
|
|
||||||
path: P,
|
|
||||||
ssh_address: S,
|
|
||||||
server_directory: D,
|
|
||||||
) -> Result<(), String>
|
|
||||||
where
|
|
||||||
P: AsRef<Path>,
|
|
||||||
S: AsRef<str>,
|
|
||||||
D: AsRef<Path>,
|
|
||||||
{
|
|
||||||
let full_path = server_directory.as_ref().join(path);
|
|
||||||
match &ShellCmd::new("ssh")
|
|
||||||
.arg(ssh_address.as_ref())
|
|
||||||
.arg(osf!("test -f ") + &full_path)
|
|
||||||
.collect_output()
|
|
||||||
{
|
|
||||||
Ok(_) => Ok(()), //file exists on file server
|
|
||||||
Err(CommandSpecificError {
|
|
||||||
error: ExecutionError::BadExitStatus(_), //test failed
|
|
||||||
..
|
|
||||||
}) => Err(format!(
|
|
||||||
"File '{}' doesn't exist on file-server",
|
|
||||||
full_path.to_string_lossy()
|
|
||||||
)),
|
|
||||||
Err(e) => Err(format!(
|
|
||||||
"Failed to check whether file exists on file-server: {e}"
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<T>(servers: &[T]) -> Result<(), String> {
|
fn require_non_empty_servers<T>(servers: &[T]) -> Result<(), String> {
|
||||||
require_non_empty(servers, "servers for this operation")
|
Self::require_non_empty(servers, "servers for this operation")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_non_empty<T>(slice: &[T], slice_name: &str) -> Result<(), String> {
|
fn require_non_empty<T>(slice: &[T], slice_name: &str) -> Result<(), String> {
|
||||||
@ -707,42 +703,97 @@ fn require_non_empty<T>(slice: &[T], slice_name: &str) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_ssh_agent(logger: &Logger) -> Result<(), String> {
|
fn start_ssh_agent(&mut self, logger: &Logger) -> Result<(), String> {
|
||||||
|
let env = &mut self.environment;
|
||||||
|
|
||||||
//start the ssh agent
|
//start the ssh agent
|
||||||
let agent_output = ShellCmd::new("ssh-agent")
|
let agent_output = ShellCommand::SshAgent
|
||||||
.arg("-s")
|
.in_env(env)
|
||||||
.collect_output()
|
.output()
|
||||||
.map_err(|e| format!("failed to start ssh agent: {e}"))?;
|
.and_expect_success()
|
||||||
let agent_stdout = String::from_utf8_lossy(&agent_output.stdout);
|
.into_result_with_error_logging(logger)
|
||||||
if !agent_output.status.success() {
|
.map_err(|e| format!("Failed to start ssh agent: {e}"))?;
|
||||||
return Err("failed to start ssh agent; maybe try to run ssh-agent manually?".to_string());
|
let agent_stdout = &agent_output
|
||||||
}
|
.stdout
|
||||||
|
.into_string()
|
||||||
|
.map_err(|_| "ssh-agent returned invalid utf-8 - how did this even happen?")?;
|
||||||
|
|
||||||
//set the env vars from the agent
|
//set the env vars from the agent
|
||||||
static ENV_VAR_REGEX: Lazy<Regex> = lazy_regex!("(.+?)=(.+?);");
|
static ENV_VAR_REGEX: Lazy<Regex> = lazy_regex!("(.+?)=(.+?);");
|
||||||
for capture in ENV_VAR_REGEX.captures_iter(&agent_stdout) {
|
for capture in ENV_VAR_REGEX.captures_iter(agent_stdout) {
|
||||||
let (_, [env_var, value]) = capture.extract();
|
let (_, [env_var, value]) = capture.extract();
|
||||||
env::set_var(env_var, value);
|
env.set_var(env_var, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
//add the ssh key
|
//add the ssh key
|
||||||
ShellCmd::new("ssh-add")
|
ShellCommand::ShhAdd
|
||||||
.run(logger)
|
.in_env(env)
|
||||||
|
.run_logged(logger)
|
||||||
|
.and_expect_success()
|
||||||
|
.into_result_with_error_logging(logger)
|
||||||
.map_err(|e| format!("failed to add ssh-key: {e}"))?;
|
.map_err(|e| format!("failed to add ssh-key: {e}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_server_configuration_from_env() -> Result<Vec<Server>, String> {
|
fn parse_server_configuration_from_env(&self) -> Result<Vec<Server>, String> {
|
||||||
env::var(SERVERS_ENV_VAR)
|
self
|
||||||
|
.environment
|
||||||
|
.var(SERVERS_ENV_VAR)
|
||||||
.map_err(|_| format!("Missing environment variable {}", SERVERS_ENV_VAR))
|
.map_err(|_| format!("Missing environment variable {}", SERVERS_ENV_VAR))
|
||||||
.and_then(|value| parse_server_configuration(&value))
|
.and_then(|value| parse_server_configuration(&value, || self.get_home_directory()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_server_configuration(config_str: &str) -> Result<Vec<Server>, String> {
|
fn get_home_directory(&self) -> Result<PathBuf, String> {
|
||||||
|
self
|
||||||
|
.environment
|
||||||
|
.get_home_directory()
|
||||||
|
.ok_or("Failed to find your home directory".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm<S>(&mut self, prompt: S, default_value: bool) -> bool
|
||||||
|
where
|
||||||
|
S: ToString,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
print!(
|
||||||
|
"{}[{}]",
|
||||||
|
prompt.to_string(),
|
||||||
|
if default_value { "Y|n" } else { "y|N" }
|
||||||
|
);
|
||||||
|
io::stdout().flush().expect("failed to flush stdout");
|
||||||
|
let line = self
|
||||||
|
.environment
|
||||||
|
.read_line()
|
||||||
|
.expect("Failed to read console input");
|
||||||
|
match line.to_lowercase().as_str() {
|
||||||
|
"" => return default_value,
|
||||||
|
"y" | "yes" => return true,
|
||||||
|
"n" | "no" => return false,
|
||||||
|
_ => println!("Invalid input, please choose one of the provided options"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), String> {
|
||||||
|
Application::<Prod>::default().run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_server_configuration<F>(
|
||||||
|
config_str: &str,
|
||||||
|
get_home_directory: F,
|
||||||
|
) -> Result<Vec<Server>, String>
|
||||||
|
where
|
||||||
|
F: Fn() -> Result<PathBuf, String>,
|
||||||
|
{
|
||||||
config_str
|
config_str
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(|server_entry| {
|
.map(|server_entry| {
|
||||||
Server::from_str(server_entry, RelativeLocalPathAnker::Home)
|
Server::from_str(
|
||||||
|
server_entry,
|
||||||
|
RelativeLocalPathAnker::Home,
|
||||||
|
&get_home_directory,
|
||||||
|
)
|
||||||
.map_err(|e| format!("Invalid server entry '{server_entry}': {e}"))
|
.map_err(|e| format!("Invalid server entry '{server_entry}': {e}"))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@ -757,8 +808,15 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_server_configuration() {
|
fn test_parse_server_configuration() {
|
||||||
let servers =
|
//setup directory structure for test
|
||||||
parse_server_configuration("foo:bar,.:fizz/buzz").expect("valid server configuration");
|
let home_dir = PathBuf::from("target/test");
|
||||||
|
const LOCAL_SERVER_DIR: &str = "fizz/buzz";
|
||||||
|
fs::create_dir_all(home_dir.join(LOCAL_SERVER_DIR)).expect("failed to create server directory");
|
||||||
|
|
||||||
|
let servers = parse_server_configuration(&format!("foo:bar,.:{LOCAL_SERVER_DIR}"), || {
|
||||||
|
Ok(home_dir.clone())
|
||||||
|
})
|
||||||
|
.expect("valid server configuration");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vec![
|
vec![
|
||||||
Server {
|
Server {
|
||||||
@ -769,7 +827,10 @@ mod test {
|
|||||||
},
|
},
|
||||||
Server {
|
Server {
|
||||||
address: ServerAddress::Localhost,
|
address: ServerAddress::Localhost,
|
||||||
server_directory_path: PathBuf::from("fizz/buzz"),
|
server_directory_path: home_dir
|
||||||
|
.join(LOCAL_SERVER_DIR)
|
||||||
|
.canonicalize()
|
||||||
|
.expect("home dir exists"),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
servers
|
servers
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
use crate::get_home_directory;
|
|
||||||
use std::cell::LazyCell;
|
use std::cell::LazyCell;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
@ -6,7 +5,6 @@ use std::fs;
|
|||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ServerReference {
|
pub enum ServerReference {
|
||||||
@ -15,6 +13,19 @@ pub enum ServerReference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ServerReference {
|
impl ServerReference {
|
||||||
|
pub fn from_str<F>(s: &str, get_home_directory: F) -> Result<Self, ServerReferenceParseError>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<PathBuf, String>,
|
||||||
|
{
|
||||||
|
Server::from_str(
|
||||||
|
s,
|
||||||
|
RelativeLocalPathAnker::CurrentDirectory,
|
||||||
|
get_home_directory,
|
||||||
|
)
|
||||||
|
.map(Self::Resolved)
|
||||||
|
.or_else(|_| Ok(Self::Identifier(s.to_string())))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_identifier(&self) -> &str {
|
pub fn get_identifier(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
ServerReference::Resolved(server) => server.address.identifier(),
|
ServerReference::Resolved(server) => server.address.identifier(),
|
||||||
@ -68,16 +79,6 @@ impl ServerReference {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ServerReference {
|
|
||||||
type Err = ServerReferenceParseError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Server::from_str(s, RelativeLocalPathAnker::CurrentDirectory)
|
|
||||||
.map(Self::Resolved)
|
|
||||||
.or_else(|_| Ok(Self::Identifier(s.to_string())))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for ServerReference {
|
impl PartialEq for ServerReference {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.get_identifier() == other.get_identifier()
|
self.get_identifier() == other.get_identifier()
|
||||||
@ -126,10 +127,14 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_str(
|
pub fn from_str<F>(
|
||||||
s: &str,
|
s: &str,
|
||||||
relative_local_path_anker: RelativeLocalPathAnker,
|
relative_local_path_anker: RelativeLocalPathAnker,
|
||||||
) -> Result<Self, ServerParseError> {
|
get_home_directory: F,
|
||||||
|
) -> Result<Self, ServerParseError>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<PathBuf, String>,
|
||||||
|
{
|
||||||
s.split_once(':')
|
s.split_once(':')
|
||||||
.ok_or(ServerParseError::MissingServerDirectory)
|
.ok_or(ServerParseError::MissingServerDirectory)
|
||||||
.and_then(|(identifier, server_directory)| {
|
.and_then(|(identifier, server_directory)| {
|
||||||
@ -245,13 +250,12 @@ impl Error for ServerParseError {}
|
|||||||
mod test_server_reference {
|
mod test_server_reference {
|
||||||
use crate::server::{Server, ServerAddress, ServerReference};
|
use crate::server::{Server, ServerAddress, ServerReference};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from_str() {
|
fn test_from_str() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ServerReference::Identifier("foo".to_string()),
|
ServerReference::Identifier("foo".to_string()),
|
||||||
ServerReference::from_str("foo").unwrap()
|
ServerReference::from_str("foo", || panic!("shouldn't be called")).unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ServerReference::Resolved(Server {
|
ServerReference::Resolved(Server {
|
||||||
@ -260,7 +264,7 @@ mod test_server_reference {
|
|||||||
},
|
},
|
||||||
server_directory_path: PathBuf::from("server/creative2")
|
server_directory_path: PathBuf::from("server/creative2")
|
||||||
}),
|
}),
|
||||||
ServerReference::from_str("crea:server/creative2").unwrap()
|
ServerReference::from_str("crea:server/creative2", || panic!("shouldn't be called")).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
562
src/shell_interface.rs
Normal file
562
src/shell_interface.rs
Normal file
@ -0,0 +1,562 @@
|
|||||||
|
use crate::logger::{LogLevel, Logger};
|
||||||
|
use crate::server::{Server, ServerAddress};
|
||||||
|
use crate::{log, osf};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
use std::iter::once;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Output};
|
||||||
|
use std::{io, process};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EnvCommand<'a, E> {
|
||||||
|
command: ShellCommand,
|
||||||
|
environment: &'a mut E,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> EnvCommand<'_, E>
|
||||||
|
where
|
||||||
|
E: ShellInterface,
|
||||||
|
{
|
||||||
|
pub fn run(self) -> CommandResult<ExitStatus, StartError> {
|
||||||
|
self.environment.run_command(self.command)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn output(self) -> CommandResult<CommandOutput, StartError> {
|
||||||
|
self.environment.collect_command_output(self.command)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_logged(self, logger: &Logger) -> CommandResult<LoggedRunOutput, StartError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
match logger.level {
|
||||||
|
LogLevel::Debug | LogLevel::Info => {
|
||||||
|
let res = self.run();
|
||||||
|
CommandResult {
|
||||||
|
result: res.result.map(LoggedRunOutput::from),
|
||||||
|
command: res.command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogLevel::Error => {
|
||||||
|
let res = self.output();
|
||||||
|
CommandResult {
|
||||||
|
result: res.result.map(LoggedRunOutput::from),
|
||||||
|
command: res.command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ShellCommand {
|
||||||
|
Ssh {
|
||||||
|
address: String,
|
||||||
|
server_command: ServerCommand,
|
||||||
|
},
|
||||||
|
Scp {
|
||||||
|
source: ScpParam,
|
||||||
|
destination: ScpParam,
|
||||||
|
},
|
||||||
|
SshAgent,
|
||||||
|
ShhAdd,
|
||||||
|
Editor(Vec<OsString>),
|
||||||
|
Execute {
|
||||||
|
working_directory: PathBuf,
|
||||||
|
command: Vec<OsString>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellCommand {
|
||||||
|
pub fn in_env<E>(self, environment: &mut E) -> EnvCommand<E> {
|
||||||
|
EnvCommand {
|
||||||
|
command: self,
|
||||||
|
environment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ServerCommand {
|
||||||
|
Realpath {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Ls {
|
||||||
|
dir: PathBuf,
|
||||||
|
},
|
||||||
|
Rm {
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
Mv {
|
||||||
|
source: PathBuf,
|
||||||
|
destination: PathBuf,
|
||||||
|
},
|
||||||
|
Execute {
|
||||||
|
working_directory: PathBuf,
|
||||||
|
command: OsString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScpParam {
|
||||||
|
pub server: Option<String>,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P> From<(&Server, P)> for ScpParam
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
fn from((server, path): (&Server, P)) -> Self {
|
||||||
|
Self {
|
||||||
|
server: match &server.address {
|
||||||
|
ServerAddress::Ssh { ssh_address } => Some(ssh_address.into()),
|
||||||
|
ServerAddress::Localhost => None,
|
||||||
|
},
|
||||||
|
path: PathBuf::from(path.as_ref()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Path> for ScpParam {
|
||||||
|
fn from(value: &Path) -> Self {
|
||||||
|
Self {
|
||||||
|
server: None,
|
||||||
|
path: PathBuf::from(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ScpParam> for OsString {
|
||||||
|
fn from(value: &ScpParam) -> Self {
|
||||||
|
let mut builder = osf!();
|
||||||
|
if let Some(server) = &value.server {
|
||||||
|
builder += format!("{server}:");
|
||||||
|
}
|
||||||
|
builder += &value.path;
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ShellCommand {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
command_to_string(&build_command_from_shell_command(self))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_to_string(command: &Command) -> String {
|
||||||
|
once(command.get_program().to_string_lossy().to_string())
|
||||||
|
.chain(command.get_args().map(|arg| {
|
||||||
|
let arg_str = arg.to_string_lossy();
|
||||||
|
if arg_str.contains(' ') {
|
||||||
|
format!("\"{arg_str}\"")
|
||||||
|
} else {
|
||||||
|
arg_str.to_string()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! cmd {
|
||||||
|
($programm: expr $(, $arg:expr )*)=> {{
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut cmd = Command::new($programm);
|
||||||
|
$( cmd.arg($arg); )*
|
||||||
|
cmd
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_command_from_shell_command(shell_command: &ShellCommand) -> Command {
|
||||||
|
match shell_command {
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address,
|
||||||
|
server_command,
|
||||||
|
} => cmd!(
|
||||||
|
"ssh",
|
||||||
|
address,
|
||||||
|
match server_command {
|
||||||
|
ServerCommand::Realpath { path } => osf!("realpath -e ") + path,
|
||||||
|
ServerCommand::Ls { dir } => osf!("ls ") + dir,
|
||||||
|
ServerCommand::Rm { file } => osf!("rm ") + file,
|
||||||
|
ServerCommand::Mv {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
} => osf!("mv ") + source + " " + destination,
|
||||||
|
ServerCommand::Execute {
|
||||||
|
working_directory,
|
||||||
|
command,
|
||||||
|
} => osf!("cd ") + working_directory + "; " + command,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ShellCommand::Scp {
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
} => cmd!("scp", OsString::from(source), OsString::from(destination)),
|
||||||
|
ShellCommand::SshAgent => cmd!("ssh-agent", "-s"),
|
||||||
|
ShellCommand::ShhAdd => cmd!("ssh-add"),
|
||||||
|
ShellCommand::Editor(args) => {
|
||||||
|
let mut args = args.clone();
|
||||||
|
let mut cmd = cmd!(args.remove(0));
|
||||||
|
cmd.args(args);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
ShellCommand::Execute {
|
||||||
|
working_directory,
|
||||||
|
command,
|
||||||
|
} => {
|
||||||
|
let mut args = command.clone();
|
||||||
|
let mut cmd = cmd!(args.remove(0));
|
||||||
|
cmd.args(args).current_dir(working_directory);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ShellInterface {
|
||||||
|
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError>;
|
||||||
|
fn collect_command_output(
|
||||||
|
&mut self,
|
||||||
|
command: ShellCommand,
|
||||||
|
) -> CommandResult<CommandOutput, StartError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MaybeCast<T> {
|
||||||
|
fn maybe_cast(&self) -> Option<&T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> MaybeCast<T> for T {
|
||||||
|
fn maybe_cast(&self) -> Option<&T> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum LoggedRunOutput {
|
||||||
|
ExitStatus(ExitStatus),
|
||||||
|
CommandOutput(CommandOutput),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExitStatus> for LoggedRunOutput {
|
||||||
|
fn from(value: ExitStatus) -> Self {
|
||||||
|
Self::ExitStatus(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CommandOutput> for LoggedRunOutput {
|
||||||
|
fn from(value: CommandOutput) -> Self {
|
||||||
|
Self::CommandOutput(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ExitStatus> for LoggedRunOutput {
|
||||||
|
fn as_ref(&self) -> &ExitStatus {
|
||||||
|
match self {
|
||||||
|
LoggedRunOutput::ExitStatus(status) => status,
|
||||||
|
LoggedRunOutput::CommandOutput(output) => output.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MaybeCast<CommandOutput> for LoggedRunOutput {
|
||||||
|
fn maybe_cast(&self) -> Option<&CommandOutput> {
|
||||||
|
match self {
|
||||||
|
LoggedRunOutput::ExitStatus(_) => None,
|
||||||
|
LoggedRunOutput::CommandOutput(output) => Some(output),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandResult<T, E> {
|
||||||
|
pub command: ShellCommand,
|
||||||
|
pub result: Result<T, E>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> CommandResult<T, E> {
|
||||||
|
pub fn into_result(self) -> Result<T, CommandError<E>> {
|
||||||
|
self.result.map_err(|error| CommandError {
|
||||||
|
command: self.command,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CommandResult<T, StartError> {
|
||||||
|
pub fn and_expect_success(self) -> CommandResult<T, ExecutionError<T>>
|
||||||
|
where
|
||||||
|
T: AsRef<ExitStatus>,
|
||||||
|
{
|
||||||
|
CommandResult {
|
||||||
|
result: self.result.map_err(ExecutionError::from).and_then(|t| {
|
||||||
|
if t.as_ref().success {
|
||||||
|
Ok(t)
|
||||||
|
} else {
|
||||||
|
Err(ExecutionError::BadExitStatus(t))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
command: self.command,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CommandResult<T, ExecutionError<T>> {
|
||||||
|
pub fn into_result_with_error_logging(
|
||||||
|
self,
|
||||||
|
logger: &Logger,
|
||||||
|
) -> Result<T, CommandError<ExecutionError<T>>>
|
||||||
|
where
|
||||||
|
T: MaybeCast<CommandOutput>,
|
||||||
|
{
|
||||||
|
self.result.map_err(|error| {
|
||||||
|
if let ExecutionError::BadExitStatus(t) = &error {
|
||||||
|
if let Some(output) = t.maybe_cast() {
|
||||||
|
log!(logger, error, "{}", output.stdout.to_string_lossy());
|
||||||
|
log!(logger, error, "{}", output.stderr.to_string_lossy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CommandError {
|
||||||
|
command: self.command,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CommandOutput {
|
||||||
|
pub stdout: OsString,
|
||||||
|
pub stderr: OsString,
|
||||||
|
pub status: ExitStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Output> for CommandOutput {
|
||||||
|
fn from(value: Output) -> Self {
|
||||||
|
Self {
|
||||||
|
stdout: os_string_from_ssh_output(value.stdout),
|
||||||
|
stderr: os_string_from_ssh_output(value.stderr),
|
||||||
|
status: value.status.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn os_string_from_ssh_output(output: Vec<u8>) -> OsString {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStringExt;
|
||||||
|
OsString::from_vec(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::os::windows::ffi::OsStringExt;
|
||||||
|
OsString::from_wide(output.iter().map(|&b| b as u16).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ExitStatus> for CommandOutput {
|
||||||
|
fn as_ref(&self) -> &ExitStatus {
|
||||||
|
&self.status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExitStatus {
|
||||||
|
pub success: bool,
|
||||||
|
pub string_form: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub code: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<process::ExitStatus> for ExitStatus {
|
||||||
|
fn from(value: process::ExitStatus) -> Self {
|
||||||
|
Self {
|
||||||
|
success: value.success(),
|
||||||
|
string_form: value.to_string(),
|
||||||
|
code: value.code(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ExitStatus> for ExitStatus {
|
||||||
|
fn as_ref(&self) -> &ExitStatus {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ExitStatus {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self.string_form, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommandError<E> {
|
||||||
|
pub command: ShellCommand,
|
||||||
|
pub error: E,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> From<CommandError<E>> for String
|
||||||
|
where
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
fn from(value: CommandError<E>) -> Self {
|
||||||
|
value.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> Display for CommandError<E>
|
||||||
|
where
|
||||||
|
E: Display,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Error while running command '{}': {}",
|
||||||
|
self.command, self.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> Error for CommandError<E> where E: Error {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StartError(io::Error);
|
||||||
|
|
||||||
|
impl From<io::Error> for StartError {
|
||||||
|
fn from(value: io::Error) -> Self {
|
||||||
|
StartError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for StartError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Failed to run command: {}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for StartError {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ExecutionError<T> {
|
||||||
|
StartError(StartError),
|
||||||
|
BadExitStatus(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<StartError> for ExecutionError<T> {
|
||||||
|
fn from(value: StartError) -> Self {
|
||||||
|
Self::StartError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Display for ExecutionError<T>
|
||||||
|
where
|
||||||
|
T: AsRef<ExitStatus>,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ExecutionError::StartError(e) => Display::fmt(e, f),
|
||||||
|
ExecutionError::BadExitStatus(status) => {
|
||||||
|
write!(f, "execution failed with {}", status.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Error for ExecutionError<T> where T: AsRef<ExitStatus> + Debug {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_commands {
|
||||||
|
use crate::shell_interface::{ScpParam, ServerCommand, ShellCommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_string() {
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Realpath {
|
||||||
|
path: PathBuf::from("plugins/*.jar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "realpath -e plugins/*.jar""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Ls {
|
||||||
|
dir: PathBuf::from("creative/plugins")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "ls creative/plugins""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Rm {
|
||||||
|
file: PathBuf::from("foo.txt")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "rm foo.txt""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Mv {
|
||||||
|
source: PathBuf::from("foo"),
|
||||||
|
destination: PathBuf::from("bar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "mv foo bar""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Ssh {
|
||||||
|
address: "crea".to_string(),
|
||||||
|
server_command: ServerCommand::Execute {
|
||||||
|
working_directory: PathBuf::from(".."),
|
||||||
|
command: "sudo rm -rf *".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"ssh crea "cd ..; sudo rm -rf *""#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Scp {
|
||||||
|
source: ScpParam {
|
||||||
|
server: None,
|
||||||
|
path: PathBuf::from("target/mssh")
|
||||||
|
},
|
||||||
|
destination: ScpParam {
|
||||||
|
server: Some("crea".into()),
|
||||||
|
path: PathBuf::from("/usr/bin")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"scp target/mssh crea:/usr/bin"#
|
||||||
|
);
|
||||||
|
assert_eq!(ShellCommand::SshAgent.to_string(), r#"ssh-agent -s"#);
|
||||||
|
assert_eq!(ShellCommand::ShhAdd.to_string(), r#"ssh-add"#);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Editor(vec!["kate".into(), "-b".into(), "test.txt".into()]).to_string(),
|
||||||
|
r#"kate -b test.txt"#
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ShellCommand::Execute {
|
||||||
|
working_directory: PathBuf::from("/home/me/server"),
|
||||||
|
command: vec!["java".into(), "-jar".into(), "paper.jar".into()]
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
r#"java -jar paper.jar"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
exit(1)
|
|
||||||
Loading…
Reference in New Issue
Block a user