feat: cli args for double remote
Some checks are pending
Linux / build (push) Waiting to run
MacOS / build (push) Waiting to run
Windows / build (push) Waiting to run

This commit is contained in:
veeso
2024-10-06 21:14:26 +02:00
parent 418b132f7e
commit 4d4c71106d
23 changed files with 742 additions and 221 deletions

View File

@@ -42,6 +42,10 @@
Released on
- [**Multi Host support**](https://github.com/veeso/termscp/issues/285):
- Now it is possible to work on two different remotes `remote A -> remote B` instead of just `localhost -> remote`
- Cli arguments now accept an additional `remote-args` for the left panel.
- For more details read this issue <https://github.com/veeso/termscp/issues/285>.
- [Issue 290](https://github.com/veeso/termscp/issues/290): Password prompt was broken
## 0.15.0

View File

@@ -62,11 +62,11 @@
termscp kann mit den folgenden Optionen gestartet werden:
`termscp [Optionen]... [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [lokales-arbeitsverzeichnis]`
`termscp [Optionen]... [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [protokoll://benutzer@adresse:port:arbeitsverzeichnis] [lokales-arbeitsverzeichnis]`
ODER
`termscp [Optionen]... -b [Lesezeichen-Name] [lokales-arbeitsverzeichnis]`
`termscp [Optionen]... -b [Lesezeichen-Name] -b [Lesezeichen-Name] [lokales-arbeitsverzeichnis]`
- `-P, --password <Passwort>` wenn Adresse angegeben wird, ist das Passwort dieses Argument
- `-b, --address-as-bookmark` löst das Adressargument als Lesezeichenname auf

View File

@@ -39,11 +39,11 @@
termscp se puede iniciar con las siguientes opciones:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
OR
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` si se proporciona la dirección, la contraseña será este argumento
- `-b, --address-as-bookmark` resuelve el argumento de la dirección como un nombre de marcador

View File

@@ -37,11 +37,11 @@
termscp peut être démarré avec les options suivantes :
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
ou
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` si l'adresse est fournie, le mot de passe sera cet argument
- `-b, --address-as-bookmark` résoudre l'argument d'adresse en tant que nom de signet

View File

@@ -37,11 +37,11 @@
termscp può essere lanciato con questi argomenti:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
O
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` Se viene fornito l'argomento indirizzo, questa sarà la password utilizzata per autenticarsi
- `-b, --address-as-bookmark` risolve l'argomento indirizzo come nome di un segnalibro

View File

@@ -40,13 +40,15 @@
termscp can be started with the following options:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
OR
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` if address is provided, password will be this argument
AND any combination of the two
- `-P, --password <password>` if address is provided, password will be this argument. A password *can* be specified for each remote provided. The order must be the same of the address argument. The use of this parameter is discouraged.
- `-b, --address-as-bookmark` resolve address argument as a bookmark name
- `-q, --quiet` Disable logging
- `-v, --version` Print version info

View File

@@ -40,11 +40,11 @@
O termscp pode ser iniciado com as seguintes opções:
`termscp [opções]... [protocol://usuário@endereço:porta:diretório-trabalho] [diretório-trabalho-local]`
`termscp [opções]... [protocol://usuário@endereço:porta:diretório-trabalho] [protocol://usuário@endereço:porta:diretório-trabalho] [diretório-trabalho-local]`
OU
`termscp [opções]... -b [nome-do-favorito] [diretório-trabalho-local]`
`termscp [opções]... -b [nome-do-favorito] -b [nome-do-favorito] [diretório-trabalho-local]`
- `-P, --password <senha>` se o endereço for fornecido, a senha será este argumento
- `-b, --address-as-bookmark` resolve o argumento do endereço como um nome de favorito

View File

@@ -37,11 +37,11 @@
termscp启动时可以使用以下选项:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
`termscp [options]... [protocol://user@address:port:wrkdir] [protocol://user@address:port:wrkdir] [local-wrkdir]`
或作为
`termscp [options]... -b [bookmark-name] [local-wrkdir]`
`termscp [options]... -b [bookmark-name] -b [bookmark-name] [local-wrkdir]`
- `-P, --password <password>` 登陆密码
- `-b, --address-as-bookmark` 将地址参数解析为书签名称

View File

@@ -2,14 +2,16 @@
//!
//! `activity_manager` is the module which provides run methods and handling for activities
// Deps
// Namespaces
use std::env;
use std::path::PathBuf;
use std::time::Duration;
use remotefs_ssh::SshKeyStorage as SshKeyStorageTrait;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol, HostBridgeParams};
use crate::cli::{Remote, RemoteArgs};
use crate::filetransfer::{
FileTransferParams, FileTransferProtocol, HostBridgeParams, ProtocolParams,
};
use crate::host::HostError;
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
@@ -30,6 +32,16 @@ pub enum NextActivity {
SetupActivity,
}
pub enum Host {
HostBridge,
Remote,
}
pub enum HostParams {
HostBridge(HostBridgeParams),
Remote(FileTransferParams),
}
/// The activity manager takes care of running activities and handling them until the application has ended
pub struct ActivityManager {
context: Option<Context>,
@@ -62,10 +74,100 @@ impl ActivityManager {
})
}
/// Configure remote args
pub fn configure_remote_args(&mut self, remote_args: RemoteArgs) -> Result<(), String> {
// Set for host bridge
match remote_args.host_bridge {
Remote::Bookmark(params) => self.resolve_bookmark_name(
Host::HostBridge,
&params.name,
params.password.as_deref(),
),
Remote::Host(host_params) => self.set_host_params(
HostParams::HostBridge(HostBridgeParams::Remote(
host_params.file_transfer_params.protocol,
host_params.file_transfer_params.params,
)),
host_params.password.as_deref(),
),
Remote::None => self.set_host_params(
HostParams::HostBridge(HostBridgeParams::Localhost(
env::current_dir()
.map_err(|e| format!("Could not get current directory: {e}"))?,
)),
None,
),
}?;
// set remote
match remote_args.remote {
Remote::Bookmark(params) => {
self.resolve_bookmark_name(Host::Remote, &params.name, params.password.as_deref())
}
Remote::Host(host_params) => self.set_host_params(
HostParams::Remote(host_params.file_transfer_params),
host_params.password.as_deref(),
),
Remote::None => Ok(()),
}
}
/// Set file transfer params
pub fn set_filetransfer_params(
pub fn set_host_params(
&mut self,
mut params: FileTransferParams,
host: HostParams,
password: Option<&str>,
) -> Result<(), String> {
let (remote_local_path, remote_remote_path) = match &host {
HostParams::Remote(params) => (params.local_path.clone(), params.remote_path.clone()),
_ => (None, None),
};
let mut remote_params = match &host {
HostParams::HostBridge(HostBridgeParams::Remote(protocol, protocol_params)) => {
Some((*protocol, protocol_params.clone()))
}
HostParams::HostBridge(HostBridgeParams::Localhost(_)) => None,
HostParams::Remote(ft_params) => Some((ft_params.protocol, ft_params.params.clone())),
};
// Put params into the context
if let Some((protocol, params)) = remote_params.as_mut() {
self.resolve_password_for_protocol_params(*protocol, params, password)?;
}
match host {
HostParams::HostBridge(HostBridgeParams::Localhost(path)) => {
self.context
.as_mut()
.unwrap()
.set_host_bridge_params(HostBridgeParams::Localhost(path));
}
HostParams::HostBridge(HostBridgeParams::Remote(_, _)) => {
let (protocol, params) = remote_params.unwrap();
self.context
.as_mut()
.unwrap()
.set_host_bridge_params(HostBridgeParams::Remote(protocol, params));
}
HostParams::Remote(_) => {
let (protocol, params) = remote_params.unwrap();
let params = FileTransferParams {
local_path: remote_local_path,
remote_path: remote_remote_path,
protocol,
params,
};
self.context.as_mut().unwrap().set_remote_params(params);
}
}
Ok(())
}
fn resolve_password_for_protocol_params(
&mut self,
protocol: FileTransferProtocol,
params: &mut ProtocolParams,
password: Option<&str>,
) -> Result<(), String> {
// Set password if provided
@@ -73,13 +175,13 @@ impl ActivityManager {
if let Some(password) = password {
params.set_default_secret(password.to_string());
} else if matches!(
params.protocol,
protocol,
FileTransferProtocol::Scp | FileTransferProtocol::Sftp,
) && params.params.generic_params().is_some()
) && params.generic_params().is_some()
{
// * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password
let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config());
let generic_params = params.params.generic_params().unwrap();
let generic_params = params.generic_params().unwrap();
if storage
.resolve(
&generic_params.address,
@@ -94,7 +196,7 @@ impl ActivityManager {
"storage could not find any suitable key for {}... prompting for password",
generic_params.address
);
self.prompt_password(&mut params)?;
self.prompt_password(params)?;
} else {
debug!(
"a key is already set for {}; password is not required",
@@ -102,19 +204,19 @@ impl ActivityManager {
);
}
} else {
self.prompt_password(&mut params)?;
self.prompt_password(params)?;
}
}
// Put params into the context
self.context.as_mut().unwrap().set_ftparams(params);
Ok(())
}
/// Prompt user for password to set into params.
fn prompt_password(&mut self, params: &mut FileTransferParams) -> Result<(), String> {
fn prompt_password(&mut self, params: &mut ProtocolParams) -> Result<(), String> {
let ctx = self.context.as_mut().unwrap();
let prompt = format!("Password for {}: ", params.host_name());
match tty::read_secret_from_tty(ctx.terminal(), "Password: ") {
match tty::read_secret_from_tty(ctx.terminal(), prompt) {
Err(err) => Err(format!("Could not read password: {err}")),
Ok(Some(secret)) => {
debug!(
@@ -132,16 +234,28 @@ impl ActivityManager {
/// Returns error if bookmark is not found
pub fn resolve_bookmark_name(
&mut self,
host: Host,
bookmark_name: &str,
password: Option<&str>,
) -> Result<(), String> {
if let Some(bookmarks_client) = self.context.as_mut().unwrap().bookmarks_client_mut() {
match bookmarks_client.get_bookmark(bookmark_name) {
None => Err(format!(
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
)),
Some(params) => self.set_filetransfer_params(params, password),
}
let params = match bookmarks_client.get_bookmark(bookmark_name) {
None => {
return Err(format!(
r#"Could not resolve bookmark name: "{bookmark_name}" no such bookmark"#
))
}
Some(params) => params,
};
let params = match host {
Host::Remote => HostParams::Remote(params),
Host::HostBridge => {
HostParams::HostBridge(HostBridgeParams::Remote(params.protocol, params.params))
}
};
self.set_host_params(params, password)
} else {
Err(String::from(
"Could not resolve bookmark name: bookmarks client not initialized",
@@ -235,8 +349,17 @@ impl ActivityManager {
return None;
}
};
let host_bridge_params = match ctx.host_bridge_params() {
Some(params) => params.clone(),
None => {
error!("Failed to start FileTransferActivity: host bridge params is None");
return None;
}
};
// If ft params is None, return None
let remote_params: &FileTransferParams = match ctx.ft_params() {
let remote_params: &FileTransferParams = match ctx.remote_params() {
Some(ft_params) => ft_params,
None => {
error!("Failed to start FileTransferActivity: file transfer params is None");
@@ -244,19 +367,6 @@ impl ActivityManager {
}
};
// get local path:
// - if set in file transfer params, get it from there
// - otherwise is env current dir
// - otherwise is /
let local_wrkdir = remote_params
.local_path
.clone()
.or(std::env::current_dir().ok())
.unwrap_or(PathBuf::from("/"));
// TODO: get host params from prev activity
let host_bridge_params = HostBridgeParams::Localhost(local_wrkdir);
let mut activity: FileTransferActivity =
FileTransferActivity::new(host_bridge_params, remote_params, self.ticks);
// Prepare result

View File

@@ -2,13 +2,15 @@
//!
//! defines the types for main.rs types
mod remote;
use std::path::PathBuf;
use std::time::Duration;
use argh::FromArgs;
pub use remote::{Remote, RemoteArgs};
use crate::activity_manager::NextActivity;
use crate::filetransfer::FileTransferParams;
use crate::system::logging::LogLevel;
pub enum Task {
@@ -17,7 +19,7 @@ pub enum Task {
InstallUpdate,
}
#[derive(FromArgs)]
#[derive(Default, FromArgs)]
#[argh(description = "
where positional can be:
- [address_a] [address_b] [local-wrkdir]
@@ -39,14 +41,15 @@ pub struct Args {
#[argh(subcommand)]
pub nested: Option<ArgsSubcommands>,
/// resolve address argument as a bookmark name
#[argh(switch, short = 'b')]
pub address_as_bookmark: bool,
#[argh(option, short = 'b')]
pub bookmark: Vec<String>,
/// enable TRACE log level
#[argh(switch, short = 'D')]
pub debug: bool,
/// provide password from CLI
/// provide password from CLI; if you need to provide multiple passwords, use multiple -P flags.
/// In case just respect the order of the addresses
#[argh(option, short = 'P')]
pub password: Option<String>,
pub password: Vec<String>,
/// disable logging
#[argh(switch, short = 'q')]
pub quiet: bool,
@@ -57,10 +60,7 @@ pub struct Args {
#[argh(switch, short = 'v')]
pub version: bool,
// -- positional
#[argh(
positional,
description = "protocol://user@address:port:wrkdir local-wrkdir"
)]
#[argh(positional, description = "address1 address2 local-wrkdir")]
pub positional: Vec<String>,
}
@@ -92,7 +92,7 @@ pub struct LoadThemeArgs {
}
pub struct RunOpts {
pub remote: Remote,
pub remote: RemoteArgs,
pub ticks: Duration,
pub log_level: LogLevel,
pub task: Task,
@@ -124,45 +124,10 @@ impl RunOpts {
impl Default for RunOpts {
fn default() -> Self {
Self {
remote: Remote::None,
remote: RemoteArgs::default(),
ticks: Duration::from_millis(10),
log_level: LogLevel::Info,
task: Task::Activity(NextActivity::Authentication),
}
}
}
#[allow(clippy::large_enum_variant)]
pub enum Remote {
Bookmark(BookmarkParams),
Host(HostParams),
None,
}
pub struct BookmarkParams {
pub name: String,
pub password: Option<String>,
}
pub struct HostParams {
pub params: FileTransferParams,
pub password: Option<String>,
}
impl BookmarkParams {
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
Self {
name: name.as_ref().to_string(),
password: password.map(|x| x.as_ref().to_string()),
}
}
}
impl HostParams {
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
Self {
params,
password: password.map(|x| x.as_ref().to_string()),
}
}
}

262
src/cli/remote.rs Normal file
View File

@@ -0,0 +1,262 @@
use std::path::{Path, PathBuf};
use super::Args;
use crate::filetransfer::FileTransferParams;
use crate::utils;
/// Address type
enum AddrType {
Address,
Bookmark,
}
/// Args for remote connection
#[derive(Debug)]
pub struct RemoteArgs {
pub host_bridge: Remote,
pub remote: Remote,
pub local_dir: Option<PathBuf>,
}
impl Default for RemoteArgs {
fn default() -> Self {
Self {
host_bridge: Remote::None,
remote: Remote::None,
local_dir: None,
}
}
}
impl TryFrom<&Args> for RemoteArgs {
type Error = String;
fn try_from(args: &Args) -> Result<Self, Self::Error> {
let mut remote_args = RemoteArgs::default();
// validate arguments
match (args.bookmark.len(), args.positional.len()) {
(0, positional) if positional < 4 => Ok(()),
(1, positional) if positional < 3 => Ok(()),
(2, positional) if positional < 2 => Ok(()),
(_, _) => Err("Too many arguments".to_string()),
}?;
// parse bookmark first
let last_item_index = (args.bookmark.len() + args.positional.len())
.checked_sub(1)
.unwrap_or_default();
let mut hosts = vec![];
for (i, (addr_type, arg)) in args
.bookmark
.iter()
.map(|x| (AddrType::Bookmark, x))
.chain(args.positional.iter().map(|x| (AddrType::Address, x)))
.enumerate()
{
// check if has password
let password = args.password.get(i).cloned();
// check if is last item and so a possible local dir
if i == last_item_index && Path::new(arg).exists() {
remote_args.local_dir = Some(PathBuf::from(arg));
continue;
}
let remote = match addr_type {
AddrType::Address => Self::parse_remote_address(arg)
.map(|x| Remote::Host(HostParams::new(x, password)))?,
AddrType::Bookmark => Remote::Bookmark(BookmarkParams::new(arg, password.as_ref())),
};
// set remote
hosts.push(remote);
}
// set args based on hosts len
if hosts.len() == 1 {
remote_args.remote = hosts.pop().unwrap();
} else if hosts.len() == 2 {
remote_args.host_bridge = hosts.pop().unwrap();
remote_args.remote = hosts.pop().unwrap();
}
Ok(remote_args)
}
}
impl RemoteArgs {
/// Parse remote address
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
}
}
/// Remote argument type
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Remote {
/// Bookmark name argument
Bookmark(BookmarkParams),
/// Host argument
Host(HostParams),
/// Unspecified
None,
}
/// Bookmark parameters
#[derive(Debug)]
pub struct BookmarkParams {
/// bookmark name
pub name: String,
/// bookmark password
pub password: Option<String>,
}
/// Host parameters
#[derive(Debug)]
pub struct HostParams {
/// file transfer parameters
pub file_transfer_params: FileTransferParams,
/// host password specified in arguments
pub password: Option<String>,
}
impl BookmarkParams {
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
Self {
name: name.as_ref().to_string(),
password: password.map(|x| x.as_ref().to_string()),
}
}
}
impl HostParams {
pub fn new<S: AsRef<str>>(params: FileTransferParams, password: Option<S>) -> Self {
Self {
file_transfer_params: params,
password: password.map(|x| x.as_ref().to_string()),
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_should_make_remote_args_from_args_one_remote() {
let args = Args {
positional: vec!["scp://host1".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::None));
assert!(matches!(remote_args.remote, Remote::Host(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
fn test_should_make_remote_args_from_args_two_remotes() {
let args = Args {
positional: vec!["scp://host1".to_string(), "scp://host2".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
assert!(matches!(remote_args.remote, Remote::Host(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
fn test_should_make_remote_args_from_two_remotes_and_local_dir() {
let args = Args {
positional: vec![
"scp://host1".to_string(),
"scp://host2".to_string(),
"/home".to_string(),
],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
assert!(matches!(remote_args.remote, Remote::Host(_)));
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
}
#[test]
fn test_should_make_remote_args_from_args_one_bookmarks() {
let args = Args {
bookmark: vec!["foo".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::None));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
fn test_should_make_remote_args_from_args_two_bookmarks() {
let args = Args {
bookmark: vec!["foo".to_string(), "bar".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
fn test_should_make_remote_args_from_two_bookmarks_and_local_dir() {
let args = Args {
bookmark: vec!["foo".to_string(), "bar".to_string()],
positional: vec!["/home".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
}
#[test]
fn test_should_make_remote_args_from_one_bookmark_and_one_remote() {
let args = Args {
bookmark: vec!["foo".to_string()],
positional: vec!["scp://host1".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, None);
}
#[test]
fn test_should_make_remote_args_from_one_bookmark_and_one_remote_with_local_dir() {
let args = Args {
positional: vec!["scp://host1".to_string(), "/home".to_string()],
bookmark: vec!["foo".to_string()],
..Default::default()
};
let remote_args = RemoteArgs::try_from(&args).unwrap();
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
}
}

View File

@@ -24,6 +24,15 @@ pub enum HostBridgeParams {
Remote(FileTransferProtocol, ProtocolParams),
}
impl HostBridgeParams {
pub fn unwrap_protocol_params(&self) -> &ProtocolParams {
match self {
HostBridgeParams::Localhost(_) => panic!("Localhost has no protocol params"),
HostBridgeParams::Remote(_, params) => params,
}
}
}
/// Holds connection parameters for file transfers
#[derive(Debug, Clone)]
pub struct FileTransferParams {
@@ -43,6 +52,43 @@ pub enum ProtocolParams {
WebDAV(WebDAVProtocolParams),
}
impl ProtocolParams {
pub fn password_missing(&self) -> bool {
match self {
ProtocolParams::AwsS3(params) => params.password_missing(),
ProtocolParams::Generic(params) => params.password_missing(),
ProtocolParams::Kube(params) => params.password_missing(),
ProtocolParams::Smb(params) => params.password_missing(),
ProtocolParams::WebDAV(params) => params.password_missing(),
}
}
/// Set the secret to ft params for the default secret field for this protocol
pub fn set_default_secret(&mut self, secret: String) {
match self {
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
ProtocolParams::Generic(params) => params.set_default_secret(secret),
ProtocolParams::Kube(params) => params.set_default_secret(secret),
ProtocolParams::Smb(params) => params.set_default_secret(secret),
ProtocolParams::WebDAV(params) => params.set_default_secret(secret),
}
}
pub fn host_name(&self) -> String {
match self {
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::Kube(params) => params
.namespace
.as_ref()
.cloned()
.unwrap_or_else(|| String::from("default")),
ProtocolParams::Smb(params) => params.address.clone(),
ProtocolParams::WebDAV(params) => params.uri.clone(),
}
}
}
/// Protocol params used by most common protocols
#[derive(Debug, Clone)]
pub struct GenericProtocolParams {
@@ -77,25 +123,15 @@ impl FileTransferParams {
/// Returns whether a password is supposed to be required for this protocol params.
/// The result true is returned ONLY if the supposed secret is MISSING!!!
#[cfg(test)]
pub fn password_missing(&self) -> bool {
match &self.params {
ProtocolParams::AwsS3(params) => params.password_missing(),
ProtocolParams::Generic(params) => params.password_missing(),
ProtocolParams::Kube(params) => params.password_missing(),
ProtocolParams::Smb(params) => params.password_missing(),
ProtocolParams::WebDAV(params) => params.password_missing(),
}
self.params.password_missing()
}
/// Set the secret to ft params for the default secret field for this protocol
#[cfg(test)]
pub fn set_default_secret(&mut self, secret: String) {
match &mut self.params {
ProtocolParams::AwsS3(params) => params.set_default_secret(secret),
ProtocolParams::Generic(params) => params.set_default_secret(secret),
ProtocolParams::Kube(params) => params.set_default_secret(secret),
ProtocolParams::Smb(params) => params.set_default_secret(secret),
ProtocolParams::WebDAV(params) => params.set_default_secret(secret),
}
self.params.set_default_secret(secret);
}
}

View File

@@ -12,6 +12,18 @@ use super::HostResult;
/// implement a real bridge when the resource is first loaded on the local
/// filesystem and then processed on the remote.
pub trait HostBridge {
/// Connect to host
fn connect(&mut self) -> HostResult<()>;
/// Disconnect from host
fn disconnect(&mut self) -> HostResult<()>;
/// Returns whether the host is connected
fn is_connected(&mut self) -> bool;
/// Returns whether the host is localhost
fn is_localhost(&self) -> bool;
/// Print working directory
fn pwd(&mut self) -> HostResult<PathBuf>;

View File

@@ -62,6 +62,22 @@ impl Localhost {
}
impl HostBridge for Localhost {
fn connect(&mut self) -> HostResult<()> {
Ok(())
}
fn disconnect(&mut self) -> HostResult<()> {
Ok(())
}
fn is_connected(&mut self) -> bool {
true
}
fn is_localhost(&self) -> bool {
true
}
fn pwd(&mut self) -> HostResult<PathBuf> {
Ok(self.wrkdir.clone())
}

View File

@@ -49,8 +49,25 @@ impl From<Box<dyn RemoteFs>> for RemoteBridged {
}
impl HostBridge for RemoteBridged {
fn connect(&mut self) -> HostResult<()> {
self.remote.connect().map(|_| ()).map_err(HostError::from)
}
fn disconnect(&mut self) -> HostResult<()> {
self.remote.disconnect().map_err(HostError::from)
}
fn is_connected(&mut self) -> bool {
self.remote.is_connected()
}
fn is_localhost(&self) -> bool {
false
}
fn pwd(&mut self) -> HostResult<PathBuf> {
todo!()
debug!("Getting working directory");
self.remote.pwd().map_err(HostError::from)
}
fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult<PathBuf> {

View File

@@ -1,5 +1,13 @@
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
mod activity_manager;
mod cli;
mod config;
mod explorer;
mod filetransfer;
mod host;
mod support;
mod system;
mod ui;
mod utils;
// Crates
#[macro_use]
@@ -13,28 +21,18 @@ extern crate log;
#[macro_use]
extern crate magic_crypt;
// External libs
use std::env;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::time::Duration;
// Include
mod activity_manager;
mod cli_opts;
mod config;
mod explorer;
mod filetransfer;
mod host;
mod support;
mod system;
mod ui;
mod utils;
use self::activity_manager::{ActivityManager, NextActivity};
use self::cli::{Args, ArgsSubcommands, RemoteArgs, RunOpts, Task};
use self::system::logging::{self, LogLevel};
// namespaces
use activity_manager::{ActivityManager, NextActivity};
use cli_opts::{Args, ArgsSubcommands, BookmarkParams, HostParams, Remote, RunOpts, Task};
use filetransfer::FileTransferParams;
use system::logging::{self, LogLevel};
const EXIT_CODE_SUCCESS: i32 = 0;
const EXIT_CODE_ERROR: i32 = 1;
const TERMSCP_VERSION: &str = env!("CARGO_PKG_VERSION");
const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
fn main() {
let args: Args = argh::from_env();
@@ -84,9 +82,8 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
// Match ticks
run_opts.ticks = Duration::from_millis(args.ticks);
// Remote argument
match parse_address_arg(&args) {
match RemoteArgs::try_from(&args) {
Err(err) => return Err(err),
Ok(Remote::None) => {}
Ok(remote) => {
// Set params
run_opts.remote = remote;
@@ -96,10 +93,8 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
}
// Local directory
if let Some(localdir) = args.positional.get(1) {
// Change working directory if local dir is set
let localdir: PathBuf = PathBuf::from(localdir);
if let Err(err) = env::set_current_dir(localdir.as_path()) {
if let Some(localdir) = run_opts.remote.local_dir.as_deref() {
if let Err(err) = env::set_current_dir(localdir) {
return Err(format!("Bad working directory argument: {err}"));
}
}
@@ -111,29 +106,6 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
Ok(run_opts)
}
/// Parse address argument from cli args
fn parse_address_arg(args: &Args) -> Result<Remote, String> {
if let Some(remote) = args.positional.first() {
if args.address_as_bookmark {
Ok(Remote::Bookmark(BookmarkParams::new(
remote,
args.password.as_ref(),
)))
} else {
// Parse address
parse_remote_address(remote.as_str())
.map(|x| Remote::Host(HostParams::new(x, args.password.as_deref())))
}
} else {
Ok(Remote::None)
}
}
/// Parse remote address
fn parse_remote_address(remote: &str) -> Result<FileTransferParams, String> {
utils::parser::parse_remote_opt(remote).map_err(|e| format!("Bad address option: {e}"))
}
/// Run task and return rc
fn run(run_opts: RunOpts) -> i32 {
match run_opts.task {
@@ -147,11 +119,11 @@ fn run_import_theme(theme: &Path) -> i32 {
match support::import_theme(theme) {
Ok(_) => {
println!("Theme has been successfully imported!");
0
EXIT_CODE_ERROR
}
Err(err) => {
eprintln!("{err}");
1
EXIT_CODE_ERROR
}
}
}
@@ -160,41 +132,32 @@ fn run_install_update() -> i32 {
match support::install_update() {
Ok(msg) => {
println!("{msg}");
0
EXIT_CODE_SUCCESS
}
Err(err) => {
eprintln!("Could not install update: {err}");
1
EXIT_CODE_ERROR
}
}
}
fn run_activity(activity: NextActivity, ticks: Duration, remote: Remote) -> i32 {
fn run_activity(activity: NextActivity, ticks: Duration, remote_args: RemoteArgs) -> i32 {
// Create activity manager (and context too)
let mut manager: ActivityManager = match ActivityManager::new(ticks) {
Ok(m) => m,
Err(err) => {
eprintln!("Could not start activity manager: {err}");
return 1;
return EXIT_CODE_ERROR;
}
};
// Set file transfer params if set
match remote {
Remote::Bookmark(BookmarkParams { name, password }) => {
if let Err(err) = manager.resolve_bookmark_name(&name, password.as_deref()) {
eprintln!("{err}");
return 1;
}
}
Remote::Host(HostParams { params, password }) => {
if let Err(err) = manager.set_filetransfer_params(params, password.as_deref()) {
eprintln!("{err}");
return 1;
}
}
Remote::None => {}
if let Err(err) = manager.configure_remote_args(remote_args) {
eprintln!("{err}");
return EXIT_CODE_ERROR;
}
manager.run(activity);
0
EXIT_CODE_SUCCESS
}

View File

@@ -275,7 +275,7 @@ impl Activity for AuthActivity {
fn on_create(&mut self, mut context: Context) {
debug!("Initializing activity");
// Initialize file transfer params
context.set_ftparams(FileTransferParams::default());
context.set_remote_params(FileTransferParams::default());
// Set context
self.context = Some(context);
// Clear terminal

View File

@@ -29,7 +29,7 @@ impl AuthActivity {
Ok(params) => {
self.save_recent();
// Set file transfer params to context
self.context_mut().set_ftparams(params);
self.context_mut().set_remote_params(params);
// Set exit reason
self.exit_reason = Some(super::ExitReason::Connect);
}

View File

@@ -9,7 +9,7 @@ use tuirealm::{PollStrategy, Update};
use super::browser::FileExplorerTab;
use super::{ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord, TransferPayload};
use crate::filetransfer::ProtocolParams;
use crate::filetransfer::{HostBridgeParams, ProtocolParams};
use crate::system::environment;
use crate::system::notifications::Notification;
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
@@ -105,8 +105,28 @@ impl FileTransferActivity {
/// Get remote hostname
pub(super) fn get_remote_hostname(&self) -> String {
let ft_params = self.context().ft_params().unwrap();
match &ft_params.params {
let ft_params = self.context().remote_params().unwrap();
self.get_hostname(&ft_params.params)
}
pub(super) fn get_hostbridge_hostname(&self) -> String {
let host_bridge_params = self.context().host_bridge_params().unwrap();
match host_bridge_params {
HostBridgeParams::Localhost(_) => {
let hostname = match hostname::get() {
Ok(h) => h,
Err(_) => return String::from("localhost"),
};
let hostname: String = hostname.as_os_str().to_string_lossy().to_string();
let tokens: Vec<&str> = hostname.split('.').collect();
String::from(*tokens.first().unwrap_or(&"localhost"))
}
HostBridgeParams::Remote(_, params) => self.get_hostname(params),
}
}
fn get_hostname(&self, params: &ProtocolParams) -> String {
match params {
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
ProtocolParams::Kube(params) => {
@@ -226,17 +246,10 @@ impl FileTransferActivity {
.size()
.map(|x| (x.width / 2) - 2)
.unwrap_or(0) as usize;
let hostname: String = match hostname::get() {
Ok(h) => {
let hostname: String = h.as_os_str().to_string_lossy().to_string();
let tokens: Vec<&str> = hostname.split('.').collect();
String::from(*tokens.first().unwrap_or(&"localhost"))
}
Err(_) => String::from("localhost"),
};
let hostname = self.get_hostbridge_hostname();
let hostname: String = format!(
"{}:{} ",
hostname,
"{hostname}:{} ",
fmt_path_elide_ex(
self.host_bridge().wrkdir.as_path(),
width,

View File

@@ -231,8 +231,10 @@ pub struct FileTransferActivity {
cache: Option<TempDir>,
/// Fs watcher
fswatcher: Option<FsWatcher>,
/// connected once
connected: bool,
/// host bridge connected
host_bridge_connected: bool,
/// remote connected once
remote_connected: bool,
}
impl FileTransferActivity {
@@ -244,6 +246,9 @@ impl FileTransferActivity {
) -> Self {
// Get config client
let config_client: ConfigClient = Self::init_config_client();
// init host bridge
let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client);
let host_bridge_connected = host_bridge.is_localhost();
Self {
exit_reason: None,
context: None,
@@ -253,7 +258,7 @@ impl FileTransferActivity {
.default_input_listener(ticks),
),
redraw: true,
host_bridge: HostBridgeBuilder::build(host_bridge_params, &config_client),
host_bridge,
client: RemoteFsBuilder::build(
remote_params.protocol,
remote_params.params.clone(),
@@ -274,7 +279,8 @@ impl FileTransferActivity {
None
}
},
connected: false,
host_bridge_connected,
remote_connected: false,
}
}
@@ -371,7 +377,10 @@ impl Activity for FileTransferActivity {
error!("Failed to enter raw mode: {}", err);
}
// Get files at current pwd
self.reload_host_bridge_dir();
if self.host_bridge.is_localhost() {
debug!("Reloading host bridge directory");
self.reload_host_bridge_dir();
}
debug!("Read working directory");
// Configure text editor
self.setup_text_editor();
@@ -394,15 +403,34 @@ impl Activity for FileTransferActivity {
if self.context.is_none() {
return;
}
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if (!self.client.is_connected() || !self.connected) && !self.app.mounted(&Id::FatalPopup) {
let ftparams = self.context().ft_params().unwrap();
// Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error)
if (!self.host_bridge.is_connected() || !self.host_bridge_connected)
&& !self.app.mounted(&Id::FatalPopup)
&& !self.host_bridge.is_localhost()
{
let host_bridge_params = self.context().host_bridge_params().unwrap();
let ft_params = host_bridge_params.unwrap_protocol_params();
// print params
let msg: String = Self::get_connection_msg(ft_params);
// Set init state to connecting popup
self.mount_blocking_wait(msg.as_str());
// Connect to remote
self.connect_to_host_bridge();
// Redraw
self.redraw = true;
}
// Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error)
if (!self.client.is_connected() || !self.remote_connected)
&& !self.app.mounted(&Id::FatalPopup)
&& self.host_bridge.is_connected()
{
let ftparams = self.context().remote_params().unwrap();
// print params
let msg: String = Self::get_connection_msg(&ftparams.params);
// Set init state to connecting popup
self.mount_blocking_wait(msg.as_str());
// Connect to remote
self.connect();
self.connect_to_remote();
// Redraw
self.redraw = true;
}
@@ -442,6 +470,10 @@ impl Activity for FileTransferActivity {
if self.client.is_connected() {
let _ = self.client.disconnect();
}
// disconnect host bridge
if self.host_bridge.is_connected() {
let _ = self.host_bridge.disconnect();
}
self.context.take()
}
}

View File

@@ -45,15 +45,57 @@ pub(super) enum TransferPayload {
}
impl FileTransferActivity {
pub(super) fn connect_to_host_bridge(&mut self) {
let ft_params = self.context().remote_params().unwrap().clone();
let entry_dir: Option<PathBuf> = ft_params.local_path;
// Connect to host bridge
match self.host_bridge.connect() {
Ok(()) => {
self.host_bridge_connected = self.host_bridge.is_connected();
if !self.host_bridge_connected {
return;
}
// Log welcome
self.log(
LogLevel::Info,
format!(
"Established connection with '{}'",
self.get_hostbridge_hostname()
),
);
// Try to change directory to entry directory
let mut remote_chdir: Option<PathBuf> = None;
if let Some(remote_path) = &entry_dir {
remote_chdir = Some(remote_path.clone());
}
if let Some(remote_path) = remote_chdir {
self.local_changedir(remote_path.as_path(), false);
}
// Set state to explorer
self.umount_wait();
self.reload_host_bridge_dir();
// Update file lists
self.update_host_bridge_filelist();
}
Err(err) => {
// Set popup fatal error
self.umount_wait();
self.mount_fatal(err.to_string());
}
}
}
/// Connect to remote
pub(super) fn connect(&mut self) {
let ft_params = self.context().ft_params().unwrap().clone();
pub(super) fn connect_to_remote(&mut self) {
let ft_params = self.context().remote_params().unwrap().clone();
let entry_dir: Option<PathBuf> = ft_params.remote_path;
// Connect to remote
match self.client.connect() {
Ok(Welcome { banner, .. }) => {
self.connected = self.client.is_connected();
if !self.connected {
self.remote_connected = self.client.is_connected();
if !self.remote_connected {
return;
}
@@ -119,7 +161,7 @@ impl FileTransferActivity {
/// Reload remote directory entries and update browser
pub(super) fn reload_remote_dir(&mut self) {
if !self.connected {
if !self.remote_connected {
return;
}
// Get current entries
@@ -146,11 +188,21 @@ impl FileTransferActivity {
/// Reload host_bridge directory entries and update browser
pub(super) fn reload_host_bridge_dir(&mut self) {
if !self.host_bridge_connected {
return;
}
self.mount_blocking_wait("Loading host bridge directory...");
let Ok(wrkdir) = self.host_bridge.pwd() else {
error!("failed to get host working directory");
return;
let wrkdir = match self.host_bridge.pwd() {
Ok(wrkdir) => wrkdir,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not scan current host bridge directory: {err}"),
);
return;
}
};
let res = self.host_bridge_scan(wrkdir.as_path());
@@ -1122,6 +1174,33 @@ impl FileTransferActivity {
}
}
pub(super) fn local_changedir(&mut self, path: &Path, push: bool) {
// Get current directory
let prev_dir: PathBuf = self.host_bridge().wrkdir.clone();
// Change directory
match self.host_bridge.change_wrkdir(path) {
Ok(_) => {
self.log(
LogLevel::Info,
format!("Changed directory on host bridge: {}", path.display()),
);
// Update files
self.reload_host_bridge_dir();
// Push prev_dir to stack
if push {
self.host_bridge_mut().pushd(prev_dir.as_path())
}
}
Err(err) => {
// Report err
self.log_and_alert(
LogLevel::Error,
format!("Could not change working directory: {err}"),
);
}
}
}
pub(super) fn remote_changedir(&mut self, path: &Path, push: bool) {
// Get current directory
let prev_dir: PathBuf = self.remote().wrkdir.clone();

View File

@@ -6,14 +6,15 @@
use tuirealm::terminal::TerminalBridge;
use super::store::Store;
use crate::filetransfer::FileTransferParams;
use crate::filetransfer::{FileTransferParams, HostBridgeParams};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider;
/// Context holds data structures shared by the activities
pub struct Context {
ft_params: Option<FileTransferParams>,
host_bridge_params: Option<HostBridgeParams>,
remote_params: Option<FileTransferParams>,
bookmarks_client: Option<BookmarksClient>,
config_client: ConfigClient,
pub(crate) store: Store,
@@ -33,7 +34,8 @@ impl Context {
let mut ctx = Context {
bookmarks_client,
config_client,
ft_params: None,
host_bridge_params: None,
remote_params: None,
store: Store::init(),
terminal: TerminalBridge::new().expect("Could not initialize terminal"),
theme_provider,
@@ -49,8 +51,12 @@ impl Context {
// -- getters
pub fn ft_params(&self) -> Option<&FileTransferParams> {
self.ft_params.as_ref()
pub fn remote_params(&self) -> Option<&FileTransferParams> {
self.remote_params.as_ref()
}
pub fn host_bridge_params(&self) -> Option<&HostBridgeParams> {
self.host_bridge_params.as_ref()
}
pub fn bookmarks_client(&self) -> Option<&BookmarksClient> {
@@ -91,8 +97,12 @@ impl Context {
// -- setter
pub fn set_ftparams(&mut self, params: FileTransferParams) {
self.ft_params = Some(params);
pub fn set_remote_params(&mut self, params: FileTransferParams) {
self.remote_params = Some(params);
}
pub fn set_host_bridge_params(&mut self, params: HostBridgeParams) {
self.host_bridge_params = Some(params);
}
// -- error

View File

@@ -7,7 +7,7 @@ use tuirealm::terminal::TerminalBridge;
/// Read a secret from tty with customisable prompt
pub fn read_secret_from_tty(
terminal_bridge: &mut TerminalBridge,
prompt: &str,
prompt: impl ToString,
) -> std::io::Result<Option<String>> {
let _ = terminal_bridge.disable_raw_mode();
let _ = terminal_bridge.leave_alternate_screen();