diff --git a/CHANGELOG.md b/CHANGELOG.md index 054f067..d9427a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - [Changelog](#changelog) + - [0.16.0](#0160) - [0.15.0](#0150) - [0.14.0](#0140) - [0.13.0](#0130) @@ -37,6 +38,18 @@ --- +## 0.16.0 + +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 . + - Change between auth forms with `` + - Bookmarks are automatically loaded into the last auth form. +- [Issue 290](https://github.com/veeso/termscp/issues/290): Password prompt was broken + ## 0.15.0 Released on 03/10/2024 diff --git a/docs/de/man.md b/docs/de/man.md index e4b731b..4af14e2 100644 --- a/docs/de/man.md +++ b/docs/de/man.md @@ -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 ` wenn Adresse angegeben wird, ist das Passwort dieses Argument - `-b, --address-as-bookmark` löst das Adressargument als Lesezeichenname auf diff --git a/docs/es/man.md b/docs/es/man.md index 81788a6..b1bfb8b 100644 --- a/docs/es/man.md +++ b/docs/es/man.md @@ -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 ` 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 diff --git a/docs/fr/man.md b/docs/fr/man.md index 562a543..7914f82 100644 --- a/docs/fr/man.md +++ b/docs/fr/man.md @@ -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 ` 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 diff --git a/docs/it/man.md b/docs/it/man.md index fe4e663..d4379b8 100644 --- a/docs/it/man.md +++ b/docs/it/man.md @@ -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 ` 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 diff --git a/docs/man.md b/docs/man.md index b37525a..2e418cf 100644 --- a/docs/man.md +++ b/docs/man.md @@ -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 ` if address is provided, password will be this argument +AND any combination of the two + +- `-P, --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 diff --git a/docs/ptbr/man.md b/docs/ptbr/man.md index 64da190..2b7131c 100644 --- a/docs/ptbr/man.md +++ b/docs/ptbr/man.md @@ -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 ` 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 diff --git a/docs/zh-CN/man.md b/docs/zh-CN/man.md index 9512e3e..29cb5e5 100644 --- a/docs/zh-CN/man.md +++ b/docs/zh-CN/man.md @@ -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 ` 登陆密码 - `-b, --address-as-bookmark` 将地址参数解析为书签名称 diff --git a/src/activity_manager.rs b/src/activity_manager.rs index fae07fd..89d9e5b 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -2,15 +2,17 @@ //! //! `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}; -use crate::host::{HostError, Localhost}; +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; use crate::system::environment; @@ -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, @@ -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, + ¶ms.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, ¶ms.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,17 +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(&self, params: &mut FileTransferParams) -> Result<(), String> { - match tty::read_secret_from_tty("Password: ") { + 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(), prompt) { Err(err) => Err(format!("Could not read password: {err}")), Ok(Some(secret)) => { debug!( @@ -130,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", @@ -226,15 +342,24 @@ impl ActivityManager { fn run_filetransfer(&mut self) -> Option { info!("Starting FileTransferActivity"); // Get context - let mut ctx: Context = match self.context.take() { + let ctx: Context = match self.context.take() { Some(ctx) => ctx, None => { error!("Failed to start FileTransferActivity: context is None"); 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 ft_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"); @@ -242,28 +367,8 @@ 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 = ft_params - .local_path - .clone() - .or(std::env::current_dir().ok()) - .unwrap_or(PathBuf::from("/")); - - // Prepare activity - let host: Localhost = match Localhost::new(local_wrkdir) { - Ok(host) => host, - Err(err) => { - // Set error in context - error!("Failed to initialize localhost: {}", err); - ctx.set_error(format!("Could not initialize localhost: {err}")); - return None; - } - }; let mut activity: FileTransferActivity = - FileTransferActivity::new(host, ft_params, self.ticks); + FileTransferActivity::new(host_bridge_params, remote_params, self.ticks); // Prepare result let result: Option; // Create activity diff --git a/src/cli_opts.rs b/src/cli.rs similarity index 69% rename from src/cli_opts.rs rename to src/cli.rs index ddd265c..66b9d30 100644 --- a/src/cli_opts.rs +++ b/src/cli.rs @@ -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,12 +19,14 @@ pub enum Task { InstallUpdate, } -#[derive(FromArgs)] +#[derive(Default, FromArgs)] #[argh(description = " where positional can be: - - [address] [local-wrkdir] + - [address_a] [address_b] [local-wrkdir] OR - - [bookmark-Name] [local-wrkdir] + - -b [bookmark-name_1] -b [bookmark-name_2] [local-wrkdir] + + and any combination of the above Address syntax can be: @@ -37,14 +41,15 @@ pub struct Args { #[argh(subcommand)] pub nested: Option, /// resolve address argument as a bookmark name - #[argh(switch, short = 'b')] - pub address_as_bookmark: bool, + #[argh(option, short = 'b')] + pub bookmark: Vec, /// 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, + pub password: Vec, /// disable logging #[argh(switch, short = 'q')] pub quiet: bool, @@ -55,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, } @@ -90,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, @@ -122,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, -} - -pub struct HostParams { - pub params: FileTransferParams, - pub password: Option, -} - -impl BookmarkParams { - pub fn new>(name: S, password: Option) -> Self { - Self { - name: name.as_ref().to_string(), - password: password.map(|x| x.as_ref().to_string()), - } - } -} - -impl HostParams { - pub fn new>(params: FileTransferParams, password: Option) -> Self { - Self { - params, - password: password.map(|x| x.as_ref().to_string()), - } - } -} diff --git a/src/cli/remote.rs b/src/cli/remote.rs new file mode 100644 index 0000000..3fc640f --- /dev/null +++ b/src/cli/remote.rs @@ -0,0 +1,271 @@ +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, +} + +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 { + 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 { + 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, +} + +impl Remote { + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } +} + +/// Bookmark parameters +#[derive(Debug)] +pub struct BookmarkParams { + /// bookmark name + pub name: String, + /// bookmark password + pub password: Option, +} + +/// Host parameters +#[derive(Debug)] +pub struct HostParams { + /// file transfer parameters + pub file_transfer_params: FileTransferParams, + /// host password specified in arguments + pub password: Option, +} + +impl BookmarkParams { + pub fn new>(name: S, password: Option) -> Self { + Self { + name: name.as_ref().to_string(), + password: password.map(|x| x.as_ref().to_string()), + } + } +} + +impl HostParams { + pub fn new>(params: FileTransferParams, password: Option) -> 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] + #[cfg(unix)] + 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] + #[cfg(unix)] + 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] + #[cfg(unix)] + 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"))); + } +} diff --git a/src/filetransfer/host_bridge_builder.rs b/src/filetransfer/host_bridge_builder.rs new file mode 100644 index 0000000..885f0d9 --- /dev/null +++ b/src/filetransfer/host_bridge_builder.rs @@ -0,0 +1,21 @@ +use super::{HostBridgeParams, RemoteFsBuilder}; +use crate::host::{HostBridge, Localhost, RemoteBridged}; +use crate::system::config_client::ConfigClient; + +pub struct HostBridgeBuilder; + +impl HostBridgeBuilder { + /// Build Host Bridge from parms + /// + /// if protocol and parameters are inconsistent, the function will panic. + pub fn build(params: HostBridgeParams, config_client: &ConfigClient) -> Box { + match params { + HostBridgeParams::Localhost(path) => { + Box::new(Localhost::new(path).expect("Failed to create Localhost")) + } + HostBridgeParams::Remote(protocol, params) => Box::new(RemoteBridged::from( + RemoteFsBuilder::build(protocol, params, config_client), + )), + } + } +} diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index 4eabe82..97a6583 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -2,12 +2,14 @@ //! //! `filetransfer` is the module which provides the file transfer protocols and remotefs builders -mod builder; +mod host_bridge_builder; pub mod params; +mod remotefs_builder; // -- export types -pub use builder::Builder; -pub use params::{FileTransferParams, ProtocolParams}; +pub use host_bridge_builder::HostBridgeBuilder; +pub use params::{FileTransferParams, HostBridgeParams, ProtocolParams}; +pub use remotefs_builder::RemoteFsBuilder; /// This enum defines the different transfer protocol available in termscp diff --git a/src/filetransfer/params.rs b/src/filetransfer/params.rs index bcd1eab..b98ffdf 100644 --- a/src/filetransfer/params.rs +++ b/src/filetransfer/params.rs @@ -15,6 +15,24 @@ pub use self::smb::SmbParams; pub use self::webdav::WebDAVProtocolParams; use super::FileTransferProtocol; +/// Host bridge params +#[derive(Debug, Clone)] +pub enum HostBridgeParams { + /// Localhost with starting working directory + Localhost(PathBuf), + /// Remote host with protocol and file transfer params + 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 { @@ -34,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 { @@ -68,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); } } diff --git a/src/filetransfer/builder.rs b/src/filetransfer/remotefs_builder.rs similarity index 94% rename from src/filetransfer/builder.rs rename to src/filetransfer/remotefs_builder.rs index 1845281..3a4fdc5 100644 --- a/src/filetransfer/builder.rs +++ b/src/filetransfer/remotefs_builder.rs @@ -27,9 +27,9 @@ use crate::system::sshkey_storage::SshKeyStorage; use crate::utils::ssh as ssh_utils; /// Remotefs builder -pub struct Builder; +pub struct RemoteFsBuilder; -impl Builder { +impl RemoteFsBuilder { /// Build RemoteFs client from protocol and params. /// /// if protocol and parameters are inconsistent, the function will panic. @@ -262,7 +262,7 @@ mod test { .session_token(Some("gerry-scotti")), ); let config_client = get_config_client(); - let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client); + let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client); } #[test] @@ -275,7 +275,7 @@ mod test { .password(Some("qwerty123")), ); let config_client = get_config_client(); - let _ = Builder::build(FileTransferProtocol::Ftp(true), params, &config_client); + let _ = RemoteFsBuilder::build(FileTransferProtocol::Ftp(true), params, &config_client); } #[test] @@ -288,7 +288,7 @@ mod test { client_key: Some("client_key".to_string()), }); let config_client = get_config_client(); - let _ = Builder::build(FileTransferProtocol::Kube, params, &config_client); + let _ = RemoteFsBuilder::build(FileTransferProtocol::Kube, params, &config_client); } #[test] @@ -301,7 +301,7 @@ mod test { .password(Some("qwerty123")), ); let config_client = get_config_client(); - let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client); + let _ = RemoteFsBuilder::build(FileTransferProtocol::Scp, params, &config_client); } #[test] @@ -314,7 +314,7 @@ mod test { .password(Some("qwerty123")), ); let config_client = get_config_client(); - let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client); + let _ = RemoteFsBuilder::build(FileTransferProtocol::Sftp, params, &config_client); } #[test] @@ -322,7 +322,7 @@ mod test { fn should_build_smb_fs() { let params = ProtocolParams::Smb(SmbParams::new("localhost", "share")); let config_client = get_config_client(); - let _ = Builder::build(FileTransferProtocol::Smb, params, &config_client); + let _ = RemoteFsBuilder::build(FileTransferProtocol::Smb, params, &config_client); } #[test] @@ -336,7 +336,7 @@ mod test { .password(Some("qwerty123")), ); let config_client = get_config_client(); - let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client); + let _ = RemoteFsBuilder::build(FileTransferProtocol::AwsS3, params, &config_client); } fn get_config_client() -> ConfigClient { diff --git a/src/host/bridge.rs b/src/host/bridge.rs new file mode 100644 index 0000000..e488121 --- /dev/null +++ b/src/host/bridge.rs @@ -0,0 +1,84 @@ +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use remotefs::fs::{Metadata, UnixPex}; +use remotefs::File; + +use super::HostResult; + +/// Trait to bridge a remote filesystem to the host filesystem +/// +/// In case of `Localhost` this should be effortless, while for remote hosts this should +/// 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; + + /// Change working directory with the new provided directory + fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult; + + /// Make a directory at path and update the file list (only if relative) + fn mkdir(&mut self, dir_name: &Path) -> HostResult<()> { + self.mkdir_ex(dir_name, false) + } + + /// Extended option version of makedir. + /// ignex: don't report error if directory already exists + fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()>; + + /// Remove file entry + fn remove(&mut self, entry: &File) -> HostResult<()>; + + /// Rename file or directory to new name + fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()>; + + /// Copy file to destination path + fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()>; + + /// Stat file and create a File + fn stat(&mut self, path: &Path) -> HostResult; + + /// Returns whether provided file path exists + fn exists(&mut self, path: &Path) -> HostResult; + + /// Get content of a directory + fn list_dir(&mut self, path: &Path) -> HostResult>; + + /// Set file stat + fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()>; + + /// Execute a command on localhost + fn exec(&mut self, cmd: &str) -> HostResult; + + /// Create a symlink from src to dst + fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()>; + + /// Change file mode to file, according to UNIX permissions + fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()>; + + /// Open file for reading + fn open_file(&mut self, file: &Path) -> HostResult>; + + /// Open file for writing + fn create_file( + &mut self, + file: &Path, + metadata: &Metadata, + ) -> HostResult>; + + /// Finalize write operation + fn finalize_write(&mut self, writer: Box) -> HostResult<()>; +} diff --git a/src/host/localhost.rs b/src/host/localhost.rs new file mode 100644 index 0000000..5534d37 --- /dev/null +++ b/src/host/localhost.rs @@ -0,0 +1,1042 @@ +use std::fs::{self, OpenOptions}; +use std::io::{Read, Write}; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt as _; +use std::path::{Path, PathBuf}; + +use filetime::FileTime; +use remotefs::fs::{FileType, Metadata, UnixPex}; +use remotefs::File; + +use super::{HostBridge, HostResult}; +use crate::host::{HostError, HostErrorType}; +use crate::utils::path; + +/// Localhost is the entity which holds the information about the current directory and host. +/// It provides functions to navigate across the local host file system +pub struct Localhost { + wrkdir: PathBuf, + files: Vec, +} + +impl Localhost { + /// Instantiates a new Localhost struct + pub fn new(wrkdir: PathBuf) -> HostResult { + debug!("Initializing localhost at {}", wrkdir.display()); + let mut host: Localhost = Localhost { + wrkdir, + files: Vec::new(), + }; + // Check if dir exists + let pwd = host.pwd()?; + + if !host.exists(&pwd)? { + error!( + "Failed to initialize localhost: {} doesn't exist", + host.wrkdir.display() + ); + return Err(HostError::new( + HostErrorType::NoSuchFileOrDirectory, + None, + host.wrkdir.as_path(), + )); + } + // Retrieve files for provided path + host.files = match host.list_dir(&pwd) { + Ok(files) => files, + Err(err) => { + error!( + "Failed to initialize localhost: could not scan wrkdir: {}", + err + ); + return Err(err); + } + }; + info!("Localhost initialized with success"); + Ok(host) + } + + /// Convert path to absolute path + fn to_path(&self, p: &Path) -> PathBuf { + path::absolutize(self.wrkdir.as_path(), p) + } +} + +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 { + Ok(self.wrkdir.clone()) + } + + fn change_wrkdir(&mut self, new_dir: &std::path::Path) -> HostResult { + let new_dir: PathBuf = self.to_path(new_dir); + info!("Changing localhost directory to {}...", new_dir.display()); + // Check whether directory exists + if !self.exists(new_dir.as_path())? { + error!("Could not change directory: No such file or directory"); + return Err(HostError::new( + HostErrorType::NoSuchFileOrDirectory, + None, + new_dir.as_path(), + )); + } + // Change directory + if let Err(err) = std::env::set_current_dir(new_dir.as_path()) { + error!("Could not enter directory: {}", err); + return Err(HostError::new( + HostErrorType::NoSuchFileOrDirectory, + Some(err), + new_dir.as_path(), + )); + } + let prev_dir: PathBuf = self.wrkdir.clone(); // Backup location + // Update working directory + // Change dir + self.wrkdir = new_dir; + // Scan new directory + let pwd = self.pwd()?; + self.files = match self.list_dir(&pwd) { + Ok(files) => files, + Err(err) => { + error!("Could not scan new directory: {}", err); + // Restore directory + self.wrkdir = prev_dir; + return Err(err); + } + }; + debug!("Changed directory to {}", self.wrkdir.display()); + Ok(self.wrkdir.clone()) + } + + fn mkdir_ex(&mut self, dir_name: &std::path::Path, ignore_existing: bool) -> HostResult<()> { + let dir_path: PathBuf = self.to_path(dir_name); + info!("Making directory {}", dir_path.display()); + // If dir already exists, return Error + if dir_path.exists() { + match ignore_existing { + true => return Ok(()), + false => { + return Err(HostError::new( + HostErrorType::FileAlreadyExists, + None, + dir_path.as_path(), + )) + } + } + } + match std::fs::create_dir(dir_path.as_path()) { + Ok(_) => { + // Update dir + if dir_name.is_relative() { + let pwd = self.pwd()?; + self.files = self.list_dir(&pwd)?; + } + info!("Created directory {}", dir_path.display()); + Ok(()) + } + Err(err) => { + error!("Could not make directory: {}", err); + Err(HostError::new( + HostErrorType::CouldNotCreateFile, + Some(err), + dir_path.as_path(), + )) + } + } + } + + fn remove(&mut self, entry: &File) -> HostResult<()> { + if entry.is_dir() { + // If file doesn't exist; return error + debug!("Removing directory {}", entry.path().display()); + if !entry.path().exists() { + error!("Directory doesn't exist"); + return Err(HostError::new( + HostErrorType::NoSuchFileOrDirectory, + None, + entry.path(), + )); + } + // Remove + match std::fs::remove_dir_all(entry.path()) { + Ok(_) => { + // Update dir + let pwd = self.pwd()?; + self.files = self.list_dir(&pwd)?; + info!("Removed directory {}", entry.path().display()); + Ok(()) + } + Err(err) => { + error!("Could not remove directory: {}", err); + Err(HostError::new( + HostErrorType::DeleteFailed, + Some(err), + entry.path(), + )) + } + } + } else { + // If file doesn't exist; return error + debug!("Removing file {}", entry.path().display()); + if !entry.path().exists() { + error!("File doesn't exist"); + return Err(HostError::new( + HostErrorType::NoSuchFileOrDirectory, + None, + entry.path(), + )); + } + // Remove + match std::fs::remove_file(entry.path()) { + Ok(_) => { + // Update dir + let pwd = self.pwd()?; + self.files = self.list_dir(&pwd)?; + info!("Removed file {}", entry.path().display()); + Ok(()) + } + Err(err) => { + error!("Could not remove file: {}", err); + Err(HostError::new( + HostErrorType::DeleteFailed, + Some(err), + entry.path(), + )) + } + } + } + } + + fn rename(&mut self, entry: &File, dst_path: &std::path::Path) -> HostResult<()> { + match std::fs::rename(entry.path(), dst_path) { + Ok(_) => { + // Scan dir + let pwd = self.pwd()?; + self.files = self.list_dir(&pwd)?; + debug!( + "Moved file {} to {}", + entry.path().display(), + dst_path.display() + ); + Ok(()) + } + Err(err) => { + error!( + "Failed to move {} to {}: {}", + entry.path().display(), + dst_path.display(), + err + ); + Err(HostError::new( + HostErrorType::CouldNotCreateFile, + Some(err), + entry.path(), + )) + } + } + } + + fn copy(&mut self, entry: &File, dst: &std::path::Path) -> HostResult<()> { + // Get absolute path of dest + let dst: PathBuf = self.to_path(dst); + info!( + "Copying file {} to {}", + entry.path().display(), + dst.display() + ); + // Match entry + if entry.is_dir() { + // If destination path doesn't exist, create destination + if !dst.exists() { + debug!("Directory {} doesn't exist; creating it", dst.display()); + self.mkdir(dst.as_path())?; + } + // Scan dir + let dir_files: Vec = self.list_dir(entry.path())?; + // Iterate files + for dir_entry in dir_files.iter() { + // Calculate dst + let mut sub_dst: PathBuf = dst.clone(); + sub_dst.push(dir_entry.name()); + // Call function recursively + self.copy(dir_entry, sub_dst.as_path())?; + } + } else { + // Copy file + // If destination path is a directory, push file name + let dst: PathBuf = match dst.as_path().is_dir() { + true => { + let mut p: PathBuf = dst.clone(); + p.push(entry.name().as_str()); + p + } + false => dst.clone(), + }; + // Copy entry path to dst path + if let Err(err) = std::fs::copy(entry.path(), dst.as_path()) { + error!("Failed to copy file: {}", err); + return Err(HostError::new( + HostErrorType::CouldNotCreateFile, + Some(err), + entry.path(), + )); + } + info!("File copied"); + } + // Reload directory if dst is pwd + let pwd = self.pwd()?; + match dst.is_dir() { + true => { + if dst == pwd { + self.files = self.list_dir(&pwd)?; + } else if let Some(parent) = dst.parent() { + // If parent is pwd, scan directory + if parent == pwd { + self.files = self.list_dir(&pwd)?; + } + } + } + false => { + if let Some(parent) = dst.parent() { + // If parent is pwd, scan directory + if parent == pwd { + self.files = self.list_dir(&pwd)?; + } + } + } + } + Ok(()) + } + + fn stat(&mut self, path: &std::path::Path) -> HostResult { + info!("Stating file {}", path.display()); + let path: PathBuf = self.to_path(path); + let attr = match fs::metadata(path.as_path()) { + Ok(metadata) => metadata, + Err(err) => { + error!("Could not read file metadata: {}", err); + return Err(HostError::new( + HostErrorType::FileNotAccessible, + Some(err), + path.as_path(), + )); + } + }; + let mut metadata = Metadata::from(attr); + if let Ok(symlink) = fs::read_link(path.as_path()) { + metadata.set_symlink(symlink); + metadata.file_type = FileType::Symlink; + } + // Match dir / file + Ok(File { path, metadata }) + } + + fn exists(&mut self, path: &Path) -> HostResult { + Ok(path.exists()) + } + + fn list_dir(&mut self, path: &Path) -> HostResult> { + info!("Reading directory {}", path.display()); + match std::fs::read_dir(path) { + Ok(e) => { + let mut fs_entries: Vec = Vec::new(); + for entry in e.flatten() { + // NOTE: 0.4.1, don't fail if stat for one file fails + match self.stat(entry.path().as_path()) { + Ok(entry) => fs_entries.push(entry), + Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e), + } + } + Ok(fs_entries) + } + Err(err) => Err(HostError::new( + HostErrorType::DirNotAccessible, + Some(err), + path, + )), + } + } + + fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> { + debug!("Setting stat for file at {}", path.display()); + if let Some(mtime) = metadata.modified { + let mtime = FileTime::from_system_time(mtime); + debug!("setting mtime {:?}", mtime); + filetime::set_file_mtime(path, mtime) + .map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?; + } + if let Some(atime) = metadata.accessed { + let atime = FileTime::from_system_time(atime); + filetime::set_file_atime(path, atime) + .map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?; + } + #[cfg(unix)] + if let Some(mode) = metadata.mode { + self.chmod(path, mode)?; + } + Ok(()) + } + + fn exec(&mut self, cmd: &str) -> HostResult { + // Make command + let args: Vec<&str> = cmd.split(' ').collect(); + let cmd: &str = args.first().unwrap(); + let argv: &[&str] = &args[1..]; + info!("Executing command: {} {:?}", cmd, argv); + match std::process::Command::new(cmd).args(argv).output() { + Ok(output) => match std::str::from_utf8(&output.stdout) { + Ok(s) => { + info!("Command output: {}", s); + Ok(s.to_string()) + } + Err(_) => Ok(String::new()), + }, + Err(err) => { + error!("Failed to run command: {}", err); + Err(HostError::new( + HostErrorType::ExecutionFailed, + Some(err), + self.wrkdir.as_path(), + )) + } + } + } + + #[cfg(unix)] + fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> { + let src = self.to_path(src); + std::os::unix::fs::symlink(dst, src.as_path()).map_err(|e| { + error!( + "Failed to create symlink at {} pointing at {}: {}", + src.display(), + dst.display(), + e + ); + HostError::new(HostErrorType::CouldNotCreateFile, Some(e), src.as_path()) + }) + } + + #[cfg(windows)] + fn symlink(&mut self, _src: &Path, _dst: &Path) -> HostResult<()> { + warn!("Cannot create symlink on Windows"); + + Err(HostError::from(HostErrorType::NotImplemented)) + } + + #[cfg(unix)] + fn chmod(&mut self, path: &std::path::Path, pex: UnixPex) -> HostResult<()> { + let path: PathBuf = self.to_path(path); + // Get metadta + match fs::metadata(path.as_path()) { + Ok(metadata) => { + let mut mpex = metadata.permissions(); + mpex.set_mode(pex.into()); + match fs::set_permissions(path.as_path(), mpex) { + Ok(_) => { + info!("Changed mode for {} to {:?}", path.display(), pex); + Ok(()) + } + Err(err) => { + error!("Could not change mode for file {}: {}", path.display(), err); + Err(HostError::new( + HostErrorType::FileNotAccessible, + Some(err), + path.as_path(), + )) + } + } + } + Err(err) => { + error!( + "Chmod failed; could not read metadata for file {}: {}", + path.display(), + err + ); + Err(HostError::new( + HostErrorType::FileNotAccessible, + Some(err), + path.as_path(), + )) + } + } + } + + #[cfg(windows)] + fn chmod(&mut self, _path: &std::path::Path, _pex: UnixPex) -> HostResult<()> { + warn!("Cannot set file mode on Windows"); + + Err(HostError::from(HostErrorType::NotImplemented)) + } + + fn open_file(&mut self, file: &std::path::Path) -> HostResult> { + let file: PathBuf = self.to_path(file); + info!("Opening file {} for read", file.display()); + if !self.exists(file.as_path())? { + error!("File doesn't exist!"); + return Err(HostError::new( + HostErrorType::NoSuchFileOrDirectory, + None, + file.as_path(), + )); + } + match OpenOptions::new() + .create(false) + .read(true) + .write(false) + .open(file.as_path()) + { + Ok(f) => Ok(Box::new(f)), + Err(err) => { + error!("Could not open file for read: {}", err); + Err(HostError::new( + HostErrorType::FileNotAccessible, + Some(err), + file.as_path(), + )) + } + } + } + + fn create_file( + &mut self, + file: &Path, + _metadata: &Metadata, + ) -> HostResult> { + let file: PathBuf = self.to_path(file); + info!("Opening file {} for write", file.display()); + match OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(file.as_path()) + { + Ok(f) => Ok(Box::new(f)), + Err(err) => { + error!("Failed to open file: {}", err); + match self.exists(file.as_path())? { + true => Err(HostError::new( + HostErrorType::ReadonlyFile, + Some(err), + file.as_path(), + )), + false => Err(HostError::new( + HostErrorType::FileNotAccessible, + Some(err), + file.as_path(), + )), + } + } + } + } + + fn finalize_write(&mut self, _writer: Box) -> HostResult<()> { + // no-op + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + #[cfg(unix)] + use std::fs::File as StdFile; + #[cfg(unix)] + use std::io::Write; + use std::ops::AddAssign; + #[cfg(unix)] + use std::os::unix::fs::{symlink, PermissionsExt}; + use std::time::{Duration, SystemTime}; + + use pretty_assertions::assert_eq; + + use super::*; + #[cfg(unix)] + use crate::utils::test_helpers::make_fsentry; + use crate::utils::test_helpers::{create_sample_file, make_file_at}; + + #[test] + fn test_host_error_new() { + let error: HostError = + HostError::new(HostErrorType::CouldNotCreateFile, None, Path::new("/tmp")); + assert!(error.ioerr.is_none()); + assert_eq!(error.path.as_ref().unwrap(), Path::new("/tmp")); + } + + #[test] + #[cfg(unix)] + fn test_host_localhost_new() { + let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + assert_eq!(host.wrkdir, PathBuf::from("/dev")); + // Scan dir + let entries = std::fs::read_dir(PathBuf::from("/dev").as_path()).unwrap(); + let mut counter: usize = 0; + for _ in entries { + counter += 1; + } + assert_eq!(host.files.len(), counter); + } + + #[test] + #[cfg(windows)] + fn test_host_localhost_new() { + let mut host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap(); + assert_eq!(host.wrkdir, PathBuf::from("C:\\users")); + // Scan dir + let entries = std::fs::read_dir(PathBuf::from("C:\\users").as_path()).unwrap(); + let mut counter: usize = 0; + for _ in entries { + counter = counter + 1; + } + assert_eq!(host.files.len(), counter); + } + + #[test] + #[should_panic] + fn test_host_localhost_new_bad() { + Localhost::new(PathBuf::from("/omargabber/123/345")) + .ok() + .unwrap(); + } + + #[test] + #[cfg(unix)] + fn test_host_localhost_pwd() { + let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + assert_eq!(host.pwd().unwrap(), PathBuf::from("/dev")); + } + + #[test] + #[cfg(unix)] + fn test_host_localhost_change_dir() { + let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + let new_dir: PathBuf = PathBuf::from("/dev"); + assert!(host.change_wrkdir(new_dir.as_path()).is_ok()); + // Verify new files + // Scan dir + let entries = std::fs::read_dir(new_dir.as_path()).unwrap(); + let mut counter: usize = 0; + for _ in entries { + counter += 1; + } + assert_eq!(host.files.len(), counter); + } + + #[test] + #[cfg(unix)] + #[should_panic] + fn test_host_localhost_change_dir_failed() { + let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + let new_dir: PathBuf = PathBuf::from("/omar/gabber/123/456"); + assert!(host.change_wrkdir(new_dir.as_path()).is_ok()); + } + + #[test] + #[cfg(unix)] + fn test_host_localhost_open_read() { + let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + // Create temp file + let file: tempfile::NamedTempFile = create_sample_file(); + assert!(host.open_file(file.path()).is_ok()); + } + + #[test] + #[cfg(unix)] + #[should_panic] + fn test_host_localhost_open_read_err_no_such_file() { + let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + assert!(host + .open_file(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path()) + .is_ok()); + } + + #[test] + #[cfg(any(target_os = "macos", target_os = "linux"))] + fn test_host_localhost_open_read_err_not_accessible() { + let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + let file: tempfile::NamedTempFile = create_sample_file(); + //let mut perms = fs::metadata(file.path())?.permissions(); + fs::set_permissions(file.path(), PermissionsExt::from_mode(0o222)).unwrap(); + //fs::set_permissions(file.path(), perms)?; + assert!(host.open_file(file.path()).is_err()); + } + + #[test] + #[cfg(unix)] + fn test_host_localhost_open_write() { + let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + // Create temp file + let file: tempfile::NamedTempFile = create_sample_file(); + assert!(host.create_file(file.path(), &Metadata::default()).is_ok()); + } + + #[test] + #[cfg(any(target_os = "macos", target_os = "linux"))] + fn test_host_localhost_open_write_err() { + let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); + let file: tempfile::NamedTempFile = create_sample_file(); + //let mut perms = fs::metadata(file.path())?.permissions(); + fs::set_permissions(file.path(), PermissionsExt::from_mode(0o444)).unwrap(); + //fs::set_permissions(file.path(), perms)?; + assert!(host.create_file(file.path(), &Metadata::default()).is_err()); + } + + #[cfg(unix)] + #[test] + fn test_host_localhost_symlinks() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create sample file + assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); + // Create symlink + assert!(symlink( + format!("{}/foo.txt", tmpdir.path().display()), + format!("{}/bar.txt", tmpdir.path().display()) + ) + .is_ok()); + // Get dir + let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let files: Vec = host.files.clone(); + // Verify files + let file_0: &File = files.get(0).unwrap(); + if file_0.name() == *"foo.txt" { + assert!(file_0.metadata.symlink.is_none()); + } else { + assert_eq!( + file_0.metadata.symlink.as_ref().unwrap(), + &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) + ); + } + // Verify simlink + let file_1: &File = files.get(1).unwrap(); + if file_1.name() == *"bar.txt" { + assert_eq!( + file_1.metadata.symlink.as_ref().unwrap(), + &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) + ); + } else { + assert!(file_1.metadata.symlink.is_none()); + } + } + + #[test] + #[cfg(unix)] + fn test_host_localhost_mkdir() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let files: Vec = host.files.clone(); + assert_eq!(files.len(), 0); // There should be 0 files now + assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); + let files: Vec = host.files.clone(); + assert_eq!(files.len(), 1); // There should be 1 file now + // Try to re-create directory + assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err()); + // Try abs path + assert!(host + .mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true) + .is_ok()); + // Fail + assert!(host + .mkdir_ex( + PathBuf::from("/aaaa/oooooo/tmp/test_dir_123456789").as_path(), + true + ) + .is_err()); + } + + #[test] + #[cfg(unix)] + fn test_host_localhost_remove() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create sample file + assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let files: Vec = host.files.clone(); + assert_eq!(files.len(), 1); // There should be 1 file now + // Remove file + assert!(host.remove(files.get(0).unwrap()).is_ok()); + // There should be 0 files now + let files: Vec = host.files.clone(); + assert_eq!(files.len(), 0); // There should be 0 files now + // Create directory + assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); + // Delete directory + let files: Vec = host.files.clone(); + assert_eq!(files.len(), 1); // There should be 1 file now + assert!(host.remove(files.get(0).unwrap()).is_ok()); + // Remove unexisting directory + assert!(host + .remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true)) + .is_err()); + assert!(host + .remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false)) + .is_err()); + } + + #[test] + #[cfg(unix)] + fn test_host_localhost_rename() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create sample file + let src_path: PathBuf = + PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str()); + assert!(StdFile::create(src_path.as_path()).is_ok()); + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let files: Vec = host.files.clone(); + assert_eq!(files.len(), 1); // There should be 1 file now + assert_eq!(files.get(0).unwrap().name(), "foo.txt"); + // Rename file + let dst_path: PathBuf = + PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str()); + assert!(host + .rename(files.get(0).unwrap(), dst_path.as_path()) + .is_ok()); + // There should be still 1 file now, but named bar.txt + let files: Vec = host.files.clone(); + assert_eq!(files.len(), 1); // There should be 0 files now + assert_eq!(files.get(0).unwrap().name(), "bar.txt"); + // Fail + let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu"); + assert!(host + .rename(files.get(0).unwrap(), bad_path.as_path()) + .is_err()); + } + + #[test] + fn should_setstat() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + let file: tempfile::NamedTempFile = create_sample_file(); + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + // stat + let mut filemeta = host.stat(file.path()).unwrap(); + + let mut new_atime = SystemTime::UNIX_EPOCH; + new_atime.add_assign(Duration::from_secs(1612164210)); + + let mut new_mtime = SystemTime::UNIX_EPOCH; + new_mtime.add_assign(Duration::from_secs(1613160210)); + + filemeta.metadata.accessed = Some(new_atime); + filemeta.metadata.modified = Some(new_mtime); + + // setstat + assert!(host.setstat(filemeta.path(), filemeta.metadata()).is_ok()); + let new_metadata = host.stat(file.path()).unwrap(); + + assert_eq!(new_metadata.metadata().accessed, Some(new_atime)); + assert_eq!(new_metadata.metadata().modified, Some(new_mtime)); + } + + #[cfg(unix)] + #[test] + fn test_host_chmod() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + let file: tempfile::NamedTempFile = create_sample_file(); + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + // Chmod to file + assert!(host.chmod(file.path(), UnixPex::from(0o755)).is_ok()); + // Chmod to dir + assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok()); + // Error + assert!(host + .chmod( + Path::new("/tmp/krgiogoiegj/kwrgnoerig"), + UnixPex::from(0o777) + ) + .is_err()); + } + + #[cfg(unix)] + #[test] + fn test_host_copy_file_absolute() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create file in tmpdir + let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); + file1_path.push("foo.txt"); + // Write file 1 + let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); + assert!(file1.write_all(b"Hello world!\n").is_ok()); + // Get file 2 path + let mut file2_path: PathBuf = PathBuf::from(tmpdir.path()); + file2_path.push("bar.txt"); + // Create host + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let file1_entry: File = host.files.get(0).unwrap().clone(); + assert_eq!(file1_entry.name(), String::from("foo.txt")); + // Copy + assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); + // Verify host has two files + assert_eq!(host.files.len(), 2); + // Fail copy + assert!(host + .copy( + &make_fsentry(PathBuf::from("/a/a7/a/a7a"), false), + PathBuf::from("571k422i").as_path() + ) + .is_err()); + } + + #[cfg(unix)] + #[test] + fn test_host_copy_file_relative() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create file in tmpdir + let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); + file1_path.push("foo.txt"); + // Write file 1 + let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); + assert!(file1.write_all(b"Hello world!\n").is_ok()); + // Get file 2 path + let file2_path: PathBuf = PathBuf::from("bar.txt"); + // Create host + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let file1_entry: File = host.files.get(0).unwrap().clone(); + assert_eq!(file1_entry.name(), String::from("foo.txt")); + // Copy + assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); + // Verify host has two files + assert_eq!(host.files.len(), 2); + } + + #[cfg(unix)] + #[test] + fn test_host_copy_directory_absolute() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create directory in tmpdir + let mut dir_src: PathBuf = PathBuf::from(tmpdir.path()); + dir_src.push("test_dir/"); + assert!(std::fs::create_dir(dir_src.as_path()).is_ok()); + // Create file in src dir + let mut file1_path: PathBuf = dir_src.clone(); + file1_path.push("foo.txt"); + // Write file 1 + let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); + assert!(file1.write_all(b"Hello world!\n").is_ok()); + // Copy dir src to dir ddest + let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path()); + dir_dest.push("test_dest_dir/"); + // Create host + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let dir_src_entry: File = host.files.get(0).unwrap().clone(); + assert_eq!(dir_src_entry.name(), String::from("test_dir")); + // Copy + assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); + // Verify host has two files + assert_eq!(host.files.len(), 2); + // Verify dir_dest contains foo.txt + let mut test_file_path: PathBuf = dir_dest.clone(); + test_file_path.push("foo.txt"); + assert!(host.stat(test_file_path.as_path()).is_ok()); + } + + #[cfg(unix)] + #[test] + fn test_host_copy_directory_relative() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + // Create directory in tmpdir + let mut dir_src: PathBuf = PathBuf::from(tmpdir.path()); + dir_src.push("test_dir/"); + assert!(std::fs::create_dir(dir_src.as_path()).is_ok()); + // Create file in src dir + let mut file1_path: PathBuf = dir_src.clone(); + file1_path.push("foo.txt"); + // Write file 1 + let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); + assert!(file1.write_all(b"Hello world!\n").is_ok()); + // Copy dir src to dir ddest + let dir_dest: PathBuf = PathBuf::from("test_dest_dir/"); + // Create host + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + let dir_src_entry: File = host.files.get(0).unwrap().clone(); + assert_eq!(dir_src_entry.name(), String::from("test_dir")); + // Copy + assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); + // Verify host has two files + assert_eq!(host.files.len(), 2); + // Verify dir_dest contains foo.txt + let mut test_file_path: PathBuf = dir_dest.clone(); + test_file_path.push("foo.txt"); + assert!(host.stat(test_file_path.as_path()).is_ok()); + } + + #[test] + fn test_host_exec() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); + // Execute + assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5")); + } + + #[cfg(unix)] + #[test] + fn should_create_symlink() { + let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); + let dir_path: &Path = tmpdir.path(); + // Make file + assert!(make_file_at(dir_path, "pippo.txt").is_ok()); + let mut host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap(); + let mut p = dir_path.to_path_buf(); + p.push("pippo.txt"); + // Make symlink + assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_ok()); + // Fail symlink + assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_err()); + assert!(host + .symlink(Path::new("/tmp/oooo/aaaa"), p.as_path()) + .is_err()); + } + + #[test] + fn test_host_fmt_error() { + let err: HostError = HostError::new( + HostErrorType::CouldNotCreateFile, + Some(std::io::Error::from(std::io::ErrorKind::AddrInUse)), + Path::new("/tmp"), + ); + assert_eq!( + format!("{err}"), + String::from("Could not create file: address in use (/tmp)"), + ); + assert_eq!( + format!("{}", HostError::from(HostErrorType::DeleteFailed)), + String::from("Could not delete file") + ); + assert_eq!( + format!("{}", HostError::from(HostErrorType::ExecutionFailed)), + String::from("Command execution failed"), + ); + assert_eq!( + format!("{}", HostError::from(HostErrorType::DirNotAccessible)), + String::from("Could not access directory"), + ); + assert_eq!( + format!("{}", HostError::from(HostErrorType::NoSuchFileOrDirectory)), + String::from("No such file or directory") + ); + assert_eq!( + format!("{}", HostError::from(HostErrorType::ReadonlyFile)), + String::from("File is readonly") + ); + assert_eq!( + format!("{}", HostError::from(HostErrorType::FileNotAccessible)), + String::from("Could not access file") + ); + assert_eq!( + format!("{}", HostError::from(HostErrorType::FileAlreadyExists)), + String::from("File already exists") + ); + } +} diff --git a/src/host/mod.rs b/src/host/mod.rs index 2dc0a39..579c0d6 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -2,26 +2,24 @@ //! //! `host` is the module which provides functionalities to host file system -// ext -// Metadata ext -#[cfg(unix)] -use std::fs::set_permissions; -use std::fs::{self, File as StdFile, OpenOptions}; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; +mod bridge; +mod localhost; +mod remote_bridged; + use std::path::{Path, PathBuf}; -use filetime::{self, FileTime}; -#[cfg(unix)] -use remotefs::fs::UnixPex; -use remotefs::fs::{File, FileType, Metadata}; use thiserror::Error; // Locals -use crate::utils::path; +pub use self::bridge::HostBridge; +pub use self::localhost::Localhost; +pub use self::remote_bridged::RemoteBridged; + +pub type HostResult = Result; /// HostErrorType provides an overview of the specific host error #[derive(Error, Debug)] +#[allow(dead_code)] pub enum HostErrorType { #[error("No such file or directory")] NoSuchFileOrDirectory, @@ -39,6 +37,10 @@ pub enum HostErrorType { ExecutionFailed, #[error("Could not delete file")] DeleteFailed, + #[error("Not implemented")] + NotImplemented, + #[error("remote fs error: {0}")] + RemoteFs(#[from] remotefs::RemoteError), } /// HostError is a wrapper for the error type and the exact io error @@ -49,6 +51,12 @@ pub struct HostError { path: Option, } +impl From for HostError { + fn from(value: remotefs::RemoteError) -> Self { + HostError::from(HostErrorType::RemoteFs(value)) + } +} + impl HostError { /// Instantiates a new HostError pub(crate) fn new(error: HostErrorType, errno: Option, p: &Path) -> Self { @@ -83,964 +91,12 @@ impl std::fmt::Display for HostError { } } -/// Localhost is the entity which holds the information about the current directory and host. -/// It provides functions to navigate across the local host file system -pub struct Localhost { - wrkdir: PathBuf, - files: Vec, -} - -impl Localhost { - /// Instantiates a new Localhost struct - pub fn new(wrkdir: PathBuf) -> Result { - debug!("Initializing localhost at {}", wrkdir.display()); - let mut host: Localhost = Localhost { - wrkdir, - files: Vec::new(), - }; - // Check if dir exists - if !host.file_exists(host.wrkdir.as_path()) { - error!( - "Failed to initialize localhost: {} doesn't exist", - host.wrkdir.display() - ); - return Err(HostError::new( - HostErrorType::NoSuchFileOrDirectory, - None, - host.wrkdir.as_path(), - )); - } - // Retrieve files for provided path - host.files = match host.list_dir(host.wrkdir.as_path()) { - Ok(files) => files, - Err(err) => { - error!( - "Failed to initialize localhost: could not scan wrkdir: {}", - err - ); - return Err(err); - } - }; - info!("Localhost initialized with success"); - Ok(host) - } - - /// Print working directory - pub fn pwd(&self) -> PathBuf { - self.wrkdir.clone() - } - - /// Change working directory with the new provided directory - pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result { - let new_dir: PathBuf = self.to_path(new_dir); - info!("Changing localhost directory to {}...", new_dir.display()); - // Check whether directory exists - if !self.file_exists(new_dir.as_path()) { - error!("Could not change directory: No such file or directory"); - return Err(HostError::new( - HostErrorType::NoSuchFileOrDirectory, - None, - new_dir.as_path(), - )); - } - // Change directory - if let Err(err) = std::env::set_current_dir(new_dir.as_path()) { - error!("Could not enter directory: {}", err); - return Err(HostError::new( - HostErrorType::NoSuchFileOrDirectory, - Some(err), - new_dir.as_path(), - )); - } - let prev_dir: PathBuf = self.wrkdir.clone(); // Backup location - // Update working directory - // Change dir - self.wrkdir = new_dir; - // Scan new directory - self.files = match self.list_dir(self.wrkdir.as_path()) { - Ok(files) => files, - Err(err) => { - error!("Could not scan new directory: {}", err); - // Restore directory - self.wrkdir = prev_dir; - return Err(err); - } - }; - debug!("Changed directory to {}", self.wrkdir.display()); - Ok(self.wrkdir.clone()) - } - - /// Make a directory at path and update the file list (only if relative) - pub fn mkdir(&mut self, dir_name: &Path) -> Result<(), HostError> { - self.mkdir_ex(dir_name, false) - } - - /// Extended option version of makedir. - /// ignex: don't report error if directory already exists - pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> { - let dir_path: PathBuf = self.to_path(dir_name); - info!("Making directory {}", dir_path.display()); - // If dir already exists, return Error - if dir_path.exists() { - match ignex { - true => return Ok(()), - false => { - return Err(HostError::new( - HostErrorType::FileAlreadyExists, - None, - dir_path.as_path(), - )) - } - } - } - match std::fs::create_dir(dir_path.as_path()) { - Ok(_) => { - // Update dir - if dir_name.is_relative() { - self.files = self.list_dir(self.wrkdir.as_path())?; - } - info!("Created directory {}", dir_path.display()); - Ok(()) - } - Err(err) => { - error!("Could not make directory: {}", err); - Err(HostError::new( - HostErrorType::CouldNotCreateFile, - Some(err), - dir_path.as_path(), - )) - } - } - } - - /// Remove file entry - pub fn remove(&mut self, entry: &File) -> Result<(), HostError> { - if entry.is_dir() { - // If file doesn't exist; return error - debug!("Removing directory {}", entry.path().display()); - if !entry.path().exists() { - error!("Directory doesn't exist"); - return Err(HostError::new( - HostErrorType::NoSuchFileOrDirectory, - None, - entry.path(), - )); - } - // Remove - match std::fs::remove_dir_all(entry.path()) { - Ok(_) => { - // Update dir - self.files = self.list_dir(self.wrkdir.as_path())?; - info!("Removed directory {}", entry.path().display()); - Ok(()) - } - Err(err) => { - error!("Could not remove directory: {}", err); - Err(HostError::new( - HostErrorType::DeleteFailed, - Some(err), - entry.path(), - )) - } - } - } else { - // If file doesn't exist; return error - debug!("Removing file {}", entry.path().display()); - if !entry.path().exists() { - error!("File doesn't exist"); - return Err(HostError::new( - HostErrorType::NoSuchFileOrDirectory, - None, - entry.path(), - )); - } - // Remove - match std::fs::remove_file(entry.path()) { - Ok(_) => { - // Update dir - self.files = self.list_dir(self.wrkdir.as_path())?; - info!("Removed file {}", entry.path().display()); - Ok(()) - } - Err(err) => { - error!("Could not remove file: {}", err); - Err(HostError::new( - HostErrorType::DeleteFailed, - Some(err), - entry.path(), - )) - } - } - } - } - - /// Rename file or directory to new name - pub fn rename(&mut self, entry: &File, dst_path: &Path) -> Result<(), HostError> { - match std::fs::rename(entry.path(), dst_path) { - Ok(_) => { - // Scan dir - self.files = self.list_dir(self.wrkdir.as_path())?; - debug!( - "Moved file {} to {}", - entry.path().display(), - dst_path.display() - ); - Ok(()) - } - Err(err) => { - error!( - "Failed to move {} to {}: {}", - entry.path().display(), - dst_path.display(), - err - ); - Err(HostError::new( - HostErrorType::CouldNotCreateFile, - Some(err), - entry.path(), - )) - } - } - } - - /// Copy file to destination path - pub fn copy(&mut self, entry: &File, dst: &Path) -> Result<(), HostError> { - // Get absolute path of dest - let dst: PathBuf = self.to_path(dst); - info!( - "Copying file {} to {}", - entry.path().display(), - dst.display() - ); - // Match entry - if entry.is_dir() { - // If destination path doesn't exist, create destination - if !dst.exists() { - debug!("Directory {} doesn't exist; creating it", dst.display()); - self.mkdir(dst.as_path())?; - } - // Scan dir - let dir_files: Vec = self.list_dir(entry.path())?; - // Iterate files - for dir_entry in dir_files.iter() { - // Calculate dst - let mut sub_dst: PathBuf = dst.clone(); - sub_dst.push(dir_entry.name()); - // Call function recursively - self.copy(dir_entry, sub_dst.as_path())?; - } - } else { - // Copy file - // If destination path is a directory, push file name - let dst: PathBuf = match dst.as_path().is_dir() { - true => { - let mut p: PathBuf = dst.clone(); - p.push(entry.name().as_str()); - p - } - false => dst.clone(), - }; - // Copy entry path to dst path - if let Err(err) = std::fs::copy(entry.path(), dst.as_path()) { - error!("Failed to copy file: {}", err); - return Err(HostError::new( - HostErrorType::CouldNotCreateFile, - Some(err), - entry.path(), - )); - } - info!("File copied"); - } - // Reload directory if dst is pwd - match dst.is_dir() { - true => { - if dst == self.pwd().as_path() { - self.files = self.list_dir(self.wrkdir.as_path())?; - } else if let Some(parent) = dst.parent() { - // If parent is pwd, scan directory - if parent == self.pwd().as_path() { - self.files = self.list_dir(self.wrkdir.as_path())?; - } - } - } - false => { - if let Some(parent) = dst.parent() { - // If parent is pwd, scan directory - if parent == self.pwd().as_path() { - self.files = self.list_dir(self.wrkdir.as_path())?; - } - } - } - } - Ok(()) - } - - /// Stat file and create a File - pub fn stat(&self, path: &Path) -> Result { - info!("Stating file {}", path.display()); - let path: PathBuf = self.to_path(path); - let attr = match fs::metadata(path.as_path()) { - Ok(metadata) => metadata, - Err(err) => { - error!("Could not read file metadata: {}", err); - return Err(HostError::new( - HostErrorType::FileNotAccessible, - Some(err), - path.as_path(), - )); - } - }; - let mut metadata = Metadata::from(attr); - if let Ok(symlink) = fs::read_link(path.as_path()) { - metadata.set_symlink(symlink); - metadata.file_type = FileType::Symlink; - } - // Match dir / file - Ok(File { path, metadata }) - } - - /// Set file stat - pub fn setstat(&self, path: &Path, metadata: &Metadata) -> Result<(), HostError> { - debug!("Setting stat for file at {}", path.display()); - if let Some(mtime) = metadata.modified { - let mtime = FileTime::from_system_time(mtime); - debug!("setting mtime {:?}", mtime); - filetime::set_file_mtime(path, mtime) - .map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?; - } - if let Some(atime) = metadata.accessed { - let atime = FileTime::from_system_time(atime); - filetime::set_file_atime(path, atime) - .map_err(|e| HostError::new(HostErrorType::FileNotAccessible, Some(e), path))?; - } - #[cfg(unix)] - if let Some(mode) = metadata.mode { - self.chmod(path, mode)?; - } - Ok(()) - } - - /// Execute a command on localhost - pub fn exec(&self, cmd: &str) -> Result { - // Make command - let args: Vec<&str> = cmd.split(' ').collect(); - let cmd: &str = args.first().unwrap(); - let argv: &[&str] = &args[1..]; - info!("Executing command: {} {:?}", cmd, argv); - match std::process::Command::new(cmd).args(argv).output() { - Ok(output) => match std::str::from_utf8(&output.stdout) { - Ok(s) => { - info!("Command output: {}", s); - Ok(s.to_string()) - } - Err(_) => Ok(String::new()), - }, - Err(err) => { - error!("Failed to run command: {}", err); - Err(HostError::new( - HostErrorType::ExecutionFailed, - Some(err), - self.wrkdir.as_path(), - )) - } - } - } - - /// Change file mode to file, according to UNIX permissions - #[cfg(unix)] - pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> { - let path: PathBuf = self.to_path(path); - // Get metadta - match fs::metadata(path.as_path()) { - Ok(metadata) => { - let mut mpex = metadata.permissions(); - mpex.set_mode(pex.into()); - match set_permissions(path.as_path(), mpex) { - Ok(_) => { - info!("Changed mode for {} to {:?}", path.display(), pex); - Ok(()) - } - Err(err) => { - error!("Could not change mode for file {}: {}", path.display(), err); - Err(HostError::new( - HostErrorType::FileNotAccessible, - Some(err), - path.as_path(), - )) - } - } - } - Err(err) => { - error!( - "Chmod failed; could not read metadata for file {}: {}", - path.display(), - err - ); - Err(HostError::new( - HostErrorType::FileNotAccessible, - Some(err), - path.as_path(), - )) - } - } - } - - /// Open file for read - pub fn open_file_read(&self, file: &Path) -> Result { - let file: PathBuf = self.to_path(file); - info!("Opening file {} for read", file.display()); - if !self.file_exists(file.as_path()) { - error!("File doesn't exist!"); - return Err(HostError::new( - HostErrorType::NoSuchFileOrDirectory, - None, - file.as_path(), - )); - } - match OpenOptions::new() - .create(false) - .read(true) - .write(false) - .open(file.as_path()) - { - Ok(f) => Ok(f), - Err(err) => { - error!("Could not open file for read: {}", err); - Err(HostError::new( - HostErrorType::FileNotAccessible, - Some(err), - file.as_path(), - )) - } - } - } - - /// Open file for write - pub fn open_file_write(&self, file: &Path) -> Result { - let file: PathBuf = self.to_path(file); - info!("Opening file {} for write", file.display()); - match OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(file.as_path()) - { - Ok(f) => Ok(f), - Err(err) => { - error!("Failed to open file: {}", err); - match self.file_exists(file.as_path()) { - true => Err(HostError::new( - HostErrorType::ReadonlyFile, - Some(err), - file.as_path(), - )), - false => Err(HostError::new( - HostErrorType::FileNotAccessible, - Some(err), - file.as_path(), - )), - } - } - } - } - - /// Returns whether provided file path exists - pub fn file_exists(&self, path: &Path) -> bool { - path.exists() - } - - /// Get content of the current directory as a list of fs entry - pub fn list_dir(&self, dir: &Path) -> Result, HostError> { - info!("Reading directory {}", dir.display()); - match std::fs::read_dir(dir) { - Ok(e) => { - let mut fs_entries: Vec = Vec::new(); - for entry in e.flatten() { - // NOTE: 0.4.1, don't fail if stat for one file fails - match self.stat(entry.path().as_path()) { - Ok(entry) => fs_entries.push(entry), - Err(e) => error!("Failed to stat {}: {}", entry.path().display(), e), - } - } - Ok(fs_entries) - } - Err(err) => Err(HostError::new( - HostErrorType::DirNotAccessible, - Some(err), - dir, - )), - } - } - - /// Create a symlink at path pointing at target - #[cfg(unix)] - pub fn symlink(&self, path: &Path, target: &Path) -> Result<(), HostError> { - let path = self.to_path(path); - std::os::unix::fs::symlink(target, path.as_path()).map_err(|e| { - error!( - "Failed to create symlink at {} pointing at {}: {}", - path.display(), - target.display(), - e - ); - HostError::new(HostErrorType::CouldNotCreateFile, Some(e), path.as_path()) - }) - } - - /// Convert path to absolute path - fn to_path(&self, p: &Path) -> PathBuf { - path::absolutize(self.wrkdir.as_path(), p) - } -} - #[cfg(test)] -mod tests { - - #[cfg(unix)] - use std::fs::File as StdFile; - #[cfg(unix)] - use std::io::Write; - use std::ops::AddAssign; - #[cfg(unix)] - use std::os::unix::fs::{symlink, PermissionsExt}; - use std::time::{Duration, SystemTime}; +mod test { use pretty_assertions::assert_eq; use super::*; - #[cfg(unix)] - use crate::utils::test_helpers::make_fsentry; - use crate::utils::test_helpers::{create_sample_file, make_file_at}; - - #[test] - fn test_host_error_new() { - let error: HostError = - HostError::new(HostErrorType::CouldNotCreateFile, None, Path::new("/tmp")); - assert!(error.ioerr.is_none()); - assert_eq!(error.path.as_ref().unwrap(), Path::new("/tmp")); - } - - #[test] - #[cfg(unix)] - fn test_host_localhost_new() { - let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - assert_eq!(host.wrkdir, PathBuf::from("/dev")); - // Scan dir - let entries = std::fs::read_dir(PathBuf::from("/dev").as_path()).unwrap(); - let mut counter: usize = 0; - for _ in entries { - counter += 1; - } - assert_eq!(host.files.len(), counter); - } - - #[test] - #[cfg(windows)] - fn test_host_localhost_new() { - let host: Localhost = Localhost::new(PathBuf::from("C:\\users")).ok().unwrap(); - assert_eq!(host.wrkdir, PathBuf::from("C:\\users")); - // Scan dir - let entries = std::fs::read_dir(PathBuf::from("C:\\users").as_path()).unwrap(); - let mut counter: usize = 0; - for _ in entries { - counter = counter + 1; - } - assert_eq!(host.files.len(), counter); - } - - #[test] - #[should_panic] - fn test_host_localhost_new_bad() { - Localhost::new(PathBuf::from("/omargabber/123/345")) - .ok() - .unwrap(); - } - - #[test] - #[cfg(unix)] - fn test_host_localhost_pwd() { - let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - assert_eq!(host.pwd(), PathBuf::from("/dev")); - } - - #[test] - #[cfg(unix)] - fn test_host_localhost_change_dir() { - let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - let new_dir: PathBuf = PathBuf::from("/dev"); - assert!(host.change_wrkdir(new_dir.as_path()).is_ok()); - // Verify new files - // Scan dir - let entries = std::fs::read_dir(new_dir.as_path()).unwrap(); - let mut counter: usize = 0; - for _ in entries { - counter += 1; - } - assert_eq!(host.files.len(), counter); - } - - #[test] - #[cfg(unix)] - #[should_panic] - fn test_host_localhost_change_dir_failed() { - let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - let new_dir: PathBuf = PathBuf::from("/omar/gabber/123/456"); - assert!(host.change_wrkdir(new_dir.as_path()).is_ok()); - } - - #[test] - #[cfg(unix)] - fn test_host_localhost_open_read() { - let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - // Create temp file - let file: tempfile::NamedTempFile = create_sample_file(); - assert!(host.open_file_read(file.path()).is_ok()); - } - - #[test] - #[cfg(unix)] - #[should_panic] - fn test_host_localhost_open_read_err_no_such_file() { - let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - assert!(host - .open_file_read(PathBuf::from("/bin/foo-bar-test-omar-123-456-789.txt").as_path()) - .is_ok()); - } - - #[test] - #[cfg(any(target_os = "macos", target_os = "linux"))] - fn test_host_localhost_open_read_err_not_accessible() { - let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - let file: tempfile::NamedTempFile = create_sample_file(); - //let mut perms = fs::metadata(file.path())?.permissions(); - fs::set_permissions(file.path(), PermissionsExt::from_mode(0o222)).unwrap(); - //fs::set_permissions(file.path(), perms)?; - assert!(host.open_file_read(file.path()).is_err()); - } - - #[test] - #[cfg(unix)] - fn test_host_localhost_open_write() { - let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - // Create temp file - let file: tempfile::NamedTempFile = create_sample_file(); - assert!(host.open_file_write(file.path()).is_ok()); - } - - #[test] - #[cfg(any(target_os = "macos", target_os = "linux"))] - fn test_host_localhost_open_write_err() { - let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap(); - let file: tempfile::NamedTempFile = create_sample_file(); - //let mut perms = fs::metadata(file.path())?.permissions(); - fs::set_permissions(file.path(), PermissionsExt::from_mode(0o444)).unwrap(); - //fs::set_permissions(file.path(), perms)?; - assert!(host.open_file_write(file.path()).is_err()); - } - - #[cfg(unix)] - #[test] - fn test_host_localhost_symlinks() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - // Create sample file - assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); - // Create symlink - assert!(symlink( - format!("{}/foo.txt", tmpdir.path().display()), - format!("{}/bar.txt", tmpdir.path().display()) - ) - .is_ok()); - // Get dir - let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.files.clone(); - // Verify files - let file_0: &File = files.get(0).unwrap(); - if file_0.name() == *"foo.txt" { - assert!(file_0.metadata.symlink.is_none()); - } else { - assert_eq!( - file_0.metadata.symlink.as_ref().unwrap(), - &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) - ); - } - // Verify simlink - let file_1: &File = files.get(1).unwrap(); - if file_1.name() == *"bar.txt" { - assert_eq!( - file_1.metadata.symlink.as_ref().unwrap(), - &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) - ); - } else { - assert!(file_1.metadata.symlink.is_none()); - } - } - - #[test] - #[cfg(unix)] - fn test_host_localhost_mkdir() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.files.clone(); - assert_eq!(files.len(), 0); // There should be 0 files now - assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); - let files: Vec = host.files.clone(); - assert_eq!(files.len(), 1); // There should be 1 file now - // Try to re-create directory - assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err()); - // Try abs path - assert!(host - .mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true) - .is_ok()); - // Fail - assert!(host - .mkdir_ex( - PathBuf::from("/aaaa/oooooo/tmp/test_dir_123456789").as_path(), - true - ) - .is_err()); - } - - #[test] - #[cfg(unix)] - fn test_host_localhost_remove() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - // Create sample file - assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); - let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.files.clone(); - assert_eq!(files.len(), 1); // There should be 1 file now - // Remove file - assert!(host.remove(files.get(0).unwrap()).is_ok()); - // There should be 0 files now - let files: Vec = host.files.clone(); - assert_eq!(files.len(), 0); // There should be 0 files now - // Create directory - assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); - // Delete directory - let files: Vec = host.files.clone(); - assert_eq!(files.len(), 1); // There should be 1 file now - assert!(host.remove(files.get(0).unwrap()).is_ok()); - // Remove unexisting directory - assert!(host - .remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true)) - .is_err()); - assert!(host - .remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false)) - .is_err()); - } - - #[test] - #[cfg(unix)] - fn test_host_localhost_rename() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - // Create sample file - let src_path: PathBuf = - PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str()); - assert!(StdFile::create(src_path.as_path()).is_ok()); - let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let files: Vec = host.files.clone(); - assert_eq!(files.len(), 1); // There should be 1 file now - assert_eq!(files.get(0).unwrap().name(), "foo.txt"); - // Rename file - let dst_path: PathBuf = - PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str()); - assert!(host - .rename(files.get(0).unwrap(), dst_path.as_path()) - .is_ok()); - // There should be still 1 file now, but named bar.txt - let files: Vec = host.files.clone(); - assert_eq!(files.len(), 1); // There should be 0 files now - assert_eq!(files.get(0).unwrap().name(), "bar.txt"); - // Fail - let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu"); - assert!(host - .rename(files.get(0).unwrap(), bad_path.as_path()) - .is_err()); - } - - #[test] - fn should_setstat() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - let file: tempfile::NamedTempFile = create_sample_file(); - let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - // stat - let mut filemeta = host.stat(file.path()).unwrap(); - - let mut new_atime = SystemTime::UNIX_EPOCH; - new_atime.add_assign(Duration::from_secs(1612164210)); - - let mut new_mtime = SystemTime::UNIX_EPOCH; - new_mtime.add_assign(Duration::from_secs(1613160210)); - - filemeta.metadata.accessed = Some(new_atime); - filemeta.metadata.modified = Some(new_mtime); - - // setstat - assert!(host.setstat(filemeta.path(), filemeta.metadata()).is_ok()); - let new_metadata = host.stat(file.path()).unwrap(); - - assert_eq!(new_metadata.metadata().accessed, Some(new_atime)); - assert_eq!(new_metadata.metadata().modified, Some(new_mtime)); - } - - #[cfg(unix)] - #[test] - fn test_host_chmod() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - let file: tempfile::NamedTempFile = create_sample_file(); - let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - // Chmod to file - assert!(host.chmod(file.path(), UnixPex::from(0o755)).is_ok()); - // Chmod to dir - assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok()); - // Error - assert!(host - .chmod( - Path::new("/tmp/krgiogoiegj/kwrgnoerig"), - UnixPex::from(0o777) - ) - .is_err()); - } - - #[cfg(unix)] - #[test] - fn test_host_copy_file_absolute() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - // Create file in tmpdir - let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); - file1_path.push("foo.txt"); - // Write file 1 - let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); - assert!(file1.write_all(b"Hello world!\n").is_ok()); - // Get file 2 path - let mut file2_path: PathBuf = PathBuf::from(tmpdir.path()); - file2_path.push("bar.txt"); - // Create host - let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let file1_entry: File = host.files.get(0).unwrap().clone(); - assert_eq!(file1_entry.name(), String::from("foo.txt")); - // Copy - assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); - // Verify host has two files - assert_eq!(host.files.len(), 2); - // Fail copy - assert!(host - .copy( - &make_fsentry(PathBuf::from("/a/a7/a/a7a"), false), - PathBuf::from("571k422i").as_path() - ) - .is_err()); - } - - #[cfg(unix)] - #[test] - fn test_host_copy_file_relative() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - // Create file in tmpdir - let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); - file1_path.push("foo.txt"); - // Write file 1 - let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); - assert!(file1.write_all(b"Hello world!\n").is_ok()); - // Get file 2 path - let file2_path: PathBuf = PathBuf::from("bar.txt"); - // Create host - let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let file1_entry: File = host.files.get(0).unwrap().clone(); - assert_eq!(file1_entry.name(), String::from("foo.txt")); - // Copy - assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); - // Verify host has two files - assert_eq!(host.files.len(), 2); - } - - #[cfg(unix)] - #[test] - fn test_host_copy_directory_absolute() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - // Create directory in tmpdir - let mut dir_src: PathBuf = PathBuf::from(tmpdir.path()); - dir_src.push("test_dir/"); - assert!(std::fs::create_dir(dir_src.as_path()).is_ok()); - // Create file in src dir - let mut file1_path: PathBuf = dir_src.clone(); - file1_path.push("foo.txt"); - // Write file 1 - let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); - assert!(file1.write_all(b"Hello world!\n").is_ok()); - // Copy dir src to dir ddest - let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path()); - dir_dest.push("test_dest_dir/"); - // Create host - let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let dir_src_entry: File = host.files.get(0).unwrap().clone(); - assert_eq!(dir_src_entry.name(), String::from("test_dir")); - // Copy - assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); - // Verify host has two files - assert_eq!(host.files.len(), 2); - // Verify dir_dest contains foo.txt - let mut test_file_path: PathBuf = dir_dest.clone(); - test_file_path.push("foo.txt"); - assert!(host.stat(test_file_path.as_path()).is_ok()); - } - - #[cfg(unix)] - #[test] - fn test_host_copy_directory_relative() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - // Create directory in tmpdir - let mut dir_src: PathBuf = PathBuf::from(tmpdir.path()); - dir_src.push("test_dir/"); - assert!(std::fs::create_dir(dir_src.as_path()).is_ok()); - // Create file in src dir - let mut file1_path: PathBuf = dir_src.clone(); - file1_path.push("foo.txt"); - // Write file 1 - let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap(); - assert!(file1.write_all(b"Hello world!\n").is_ok()); - // Copy dir src to dir ddest - let dir_dest: PathBuf = PathBuf::from("test_dest_dir/"); - // Create host - let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - let dir_src_entry: File = host.files.get(0).unwrap().clone(); - assert_eq!(dir_src_entry.name(), String::from("test_dir")); - // Copy - assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); - // Verify host has two files - assert_eq!(host.files.len(), 2); - // Verify dir_dest contains foo.txt - let mut test_file_path: PathBuf = dir_dest.clone(); - test_file_path.push("foo.txt"); - assert!(host.stat(test_file_path.as_path()).is_ok()); - } - - #[test] - fn test_host_exec() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); - // Execute - assert!(host.exec("echo 5").ok().unwrap().as_str().contains("5")); - } - - #[cfg(unix)] - #[test] - fn should_create_symlink() { - let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); - let dir_path: &Path = tmpdir.path(); - // Make file - assert!(make_file_at(dir_path, "pippo.txt").is_ok()); - let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap(); - let mut p = dir_path.to_path_buf(); - p.push("pippo.txt"); - // Make symlink - assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_ok()); - // Fail symlink - assert!(host.symlink(Path::new("link.txt"), p.as_path()).is_err()); - assert!(host - .symlink(Path::new("/tmp/oooo/aaaa"), p.as_path()) - .is_err()); - } #[test] fn test_host_fmt_error() { diff --git a/src/host/remote_bridged.rs b/src/host/remote_bridged.rs new file mode 100644 index 0000000..497d9bc --- /dev/null +++ b/src/host/remote_bridged.rs @@ -0,0 +1,211 @@ +mod temp_mapped_file; + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use remotefs::fs::{Metadata, UnixPex}; +use remotefs::{File, RemoteError, RemoteErrorType, RemoteFs}; + +use self::temp_mapped_file::TempMappedFile; +use super::{HostBridge, HostError, HostResult}; + +struct WriteStreamOp { + path: PathBuf, + metadata: Metadata, + tempfile: TempMappedFile, +} + +/// A remote host bridged over the local host +pub struct RemoteBridged { + /// Remote fs client + remote: Box, + /// Reminder used to finalize write stream + write_stream_op: Option, +} + +impl RemoteBridged { + fn open_file_from_temp(&mut self, file: &Path) -> HostResult> { + let mut temp_file = TempMappedFile::new()?; + + self.remote + .open_file(file, Box::new(temp_file.clone())) + .map_err(HostError::from)?; + + // Sync changes + temp_file.sync()?; + + // now return as read + Ok(Box::new(temp_file)) + } +} + +impl From> for RemoteBridged { + fn from(remote: Box) -> Self { + RemoteBridged { + remote, + write_stream_op: None, + } + } +} + +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 { + debug!("Getting working directory"); + self.remote.pwd().map_err(HostError::from) + } + + fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult { + debug!("Changing working directory to {:?}", new_dir); + self.remote.change_dir(new_dir).map_err(HostError::from) + } + + fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()> { + debug!("Creating directory {:?}", dir_name); + match self.remote.create_dir(dir_name, UnixPex::from(0o755)) { + Ok(_) => Ok(()), + Err(remotefs::RemoteError { + kind: RemoteErrorType::DirectoryAlreadyExists, + .. + }) if ignore_existing => Ok(()), + Err(e) => Err(HostError::from(e)), + } + } + + fn remove(&mut self, entry: &File) -> HostResult<()> { + debug!("Removing {:?}", entry.path()); + if entry.is_dir() { + self.remote + .remove_dir_all(entry.path()) + .map_err(HostError::from) + } else { + self.remote + .remove_file(entry.path()) + .map_err(HostError::from) + } + } + + fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()> { + debug!("Renaming {:?} to {:?}", entry.path(), dst_path); + self.remote + .mov(entry.path(), dst_path) + .map_err(HostError::from) + } + + fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()> { + debug!("Copying {:?} to {:?}", entry.path(), dst); + self.remote.copy(entry.path(), dst).map_err(HostError::from) + } + + fn stat(&mut self, path: &Path) -> HostResult { + debug!("Statting {:?}", path); + self.remote.stat(path).map_err(HostError::from) + } + + fn exists(&mut self, path: &Path) -> HostResult { + debug!("Checking existence of {:?}", path); + self.remote.exists(path).map_err(HostError::from) + } + + fn list_dir(&mut self, path: &Path) -> HostResult> { + debug!("Listing directory {:?}", path); + self.remote.list_dir(path).map_err(HostError::from) + } + + fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()> { + debug!("Setting metadata for {:?}", path); + self.remote + .setstat(path, metadata.clone()) + .map_err(HostError::from) + } + + fn exec(&mut self, cmd: &str) -> HostResult { + debug!("Executing command: {}", cmd); + self.remote + .exec(cmd) + .map(|(_, stdout)| stdout) + .map_err(HostError::from) + } + + fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()> { + debug!("Creating symlink from {:?} to {:?}", src, dst); + self.remote.symlink(src, dst).map_err(HostError::from) + } + + fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()> { + debug!("Changing permissions of {:?} to {:?}", path, pex); + let stat = self.remote.stat(path).map_err(HostError::from)?; + let mut metadata = stat.metadata.clone(); + metadata.mode = Some(pex); + + self.setstat(path, &metadata) + } + + fn open_file(&mut self, file: &Path) -> HostResult> { + // try to use stream, otherwise download to a temporary file and return a reader + match self.remote.open(file) { + Ok(stream) => Ok(Box::new(stream)), + Err(RemoteError { + kind: RemoteErrorType::UnsupportedFeature, + .. + }) => self.open_file_from_temp(file), + Err(e) => Err(HostError::from(e)), + } + } + + fn create_file( + &mut self, + file: &Path, + metadata: &Metadata, + ) -> HostResult> { + // try to use stream, otherwise download to a temporary file and return a reader + match self.remote.create(file, metadata) { + Ok(stream) => Ok(Box::new(stream)), + Err(RemoteError { + kind: RemoteErrorType::UnsupportedFeature, + .. + }) => { + let tempfile = TempMappedFile::new()?; + self.write_stream_op = Some(WriteStreamOp { + path: file.to_path_buf(), + metadata: metadata.clone(), + tempfile: tempfile.clone(), + }); + + Ok(Box::new(tempfile)) + } + Err(e) => Err(HostError::from(e)), + } + } + + fn finalize_write(&mut self, _writer: Box) -> HostResult<()> { + if let Some(WriteStreamOp { + path, + metadata, + mut tempfile, + }) = self.write_stream_op.take() + { + // sync + tempfile.sync()?; + // write file + self.remote + .create_file(&path, &metadata, Box::new(tempfile))?; + } + Ok(()) + } +} diff --git a/src/host/remote_bridged/temp_mapped_file.rs b/src/host/remote_bridged/temp_mapped_file.rs new file mode 100644 index 0000000..11cf6f6 --- /dev/null +++ b/src/host/remote_bridged/temp_mapped_file.rs @@ -0,0 +1,120 @@ +use std::fs::File; +use std::io::{self, Read, Write}; +use std::sync::{Arc, Mutex}; + +use tempfile::NamedTempFile; + +use crate::host::{HostError, HostErrorType, HostResult}; + +/// A temporary file mapped to a remote file which has been transferred to local +/// and which supports read/write operations +#[derive(Debug, Clone)] +pub struct TempMappedFile { + tempfile: Arc, + handle: Arc>>, +} + +impl Write for TempMappedFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let rc = self.write_hnd()?; + let mut ref_mut = rc.lock().unwrap(); + ref_mut.as_mut().unwrap().write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + let rc = self.write_hnd()?; + let mut ref_mut = rc.lock().unwrap(); + ref_mut.as_mut().unwrap().flush() + } +} + +impl Read for TempMappedFile { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let rc = self.read_hnd()?; + let mut ref_mut = rc.lock().unwrap(); + ref_mut.as_mut().unwrap().read(buf) + } +} + +impl TempMappedFile { + pub fn new() -> HostResult { + NamedTempFile::new() + .map(|tempfile| TempMappedFile { + tempfile: Arc::new(tempfile), + handle: Arc::new(Mutex::new(None)), + }) + .map_err(|e| { + HostError::new( + HostErrorType::CouldNotCreateFile, + Some(e), + std::path::Path::new(""), + ) + }) + } + + /// Syncs the file to disk and frees the file handle. + /// + /// Must be called + pub fn sync(&mut self) -> HostResult<()> { + { + let mut lock = self.handle.lock().unwrap(); + + if let Some(hnd) = lock.take() { + hnd.sync_all().map_err(|e| { + HostError::new( + HostErrorType::FileNotAccessible, + Some(e), + self.tempfile.path(), + ) + })?; + } + } + + Ok(()) + } + + fn write_hnd(&mut self) -> io::Result>>> { + { + let mut lock = self.handle.lock().unwrap(); + if lock.is_none() { + let hnd = File::create(self.tempfile.path())?; + lock.replace(hnd); + } + } + + Ok(self.handle.clone()) + } + + fn read_hnd(&mut self) -> io::Result>>> { + { + let mut lock = self.handle.lock().unwrap(); + if lock.is_none() { + let hnd = File::open(self.tempfile.path())?; + lock.replace(hnd); + } + } + + Ok(self.handle.clone()) + } +} + +#[cfg(test)] +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_should_write_and_read_file() { + let mut file = TempMappedFile::new().unwrap(); + file.write_all(b"Hello, World!").unwrap(); + + file.sync().unwrap(); + + let mut buf = Vec::new(); + file.read_to_end(&mut buf).unwrap(); + + assert_eq!(buf, b"Hello, World!"); + } +} diff --git a/src/main.rs b/src/main.rs index 9e1e839..c6e0f85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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,22 +82,24 @@ fn parse_args(args: Args) -> Result { // 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; - // In this case the first activity will be FileTransfer - run_opts.task = Task::Activity(NextActivity::FileTransfer); } } + // set activity based on remote state + run_opts.task = if run_opts.remote.remote.is_none() { + Task::Activity(NextActivity::Authentication) + } else { + Task::Activity(NextActivity::FileTransfer) + }; + // 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 +111,6 @@ fn parse_args(args: Args) -> Result { Ok(run_opts) } -/// Parse address argument from cli args -fn parse_address_arg(args: &Args) -> Result { - 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 { - 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 +124,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 +137,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 } diff --git a/src/system/watcher/change.rs b/src/system/watcher/change.rs index 9a9dc9a..254edd2 100644 --- a/src/system/watcher/change.rs +++ b/src/system/watcher/change.rs @@ -136,7 +136,7 @@ impl FileToRemove { #[derive(Debug, PartialEq, Eq, Clone)] pub struct FileUpdate { /// Path to file which has changed - local: PathBuf, + host_bridge: PathBuf, /// Path to remote file to update remote: PathBuf, } @@ -152,13 +152,13 @@ impl FileUpdate { fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self { Self { remote: remote_relative_path(&changed_path, local_watched_path, remote_synched_path), - local: changed_path, + host_bridge: changed_path, } } /// Get path to local file to sync - pub fn local(&self) -> &Path { - self.local.as_path() + pub fn host_bridge(&self) -> &Path { + self.host_bridge.as_path() } /// Get path to remote file to sync @@ -288,7 +288,7 @@ mod test { Path::new("/home/foo/bar.txt"), ); if let FsChange::Update(change) = change { - assert_eq!(change.local(), Path::new("/tmp/bar.txt"),); + assert_eq!(change.host_bridge(), Path::new("/tmp/bar.txt"),); assert_eq!(change.remote(), Path::new("/home/foo/bar.txt")); } else { panic!("not an update"); @@ -303,7 +303,7 @@ mod test { Path::new("/home/foo/temp"), ); if let FsChange::Update(change) = change { - assert_eq!(change.local(), Path::new("/tmp/abc/foo.txt"),); + assert_eq!(change.host_bridge(), Path::new("/tmp/abc/foo.txt"),); assert_eq!(change.remote(), Path::new("/home/foo/temp/abc/foo.txt")); } else { panic!("not an update"); diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index fae5c6e..416a909 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -3,11 +3,12 @@ //! `auth_activity` is the module which implements the authentication activity // Locals -use super::{AuthActivity, FileTransferParams}; +use super::{AuthActivity, FileTransferParams, FormTab, HostBridgeProtocol}; use crate::filetransfer::params::{ AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams, WebDAVProtocolParams, }; +use crate::filetransfer::HostBridgeParams; impl AuthActivity { /// Delete bookmark @@ -26,27 +27,49 @@ impl AuthActivity { } /// Load selected bookmark (at index) to input fields - pub(super) fn load_bookmark(&mut self, idx: usize) { + pub(super) fn load_bookmark(&mut self, form_tab: FormTab, idx: usize) { if let Some(bookmarks_cli) = self.bookmarks_client() { // Iterate over bookmarks if let Some(key) = self.bookmarks_list.get(idx) { if let Some(bookmark) = bookmarks_cli.get_bookmark(key) { // Load parameters into components - self.load_bookmark_into_gui(bookmark); + match form_tab { + FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark), + FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark), + } } } } } /// Save current input fields as a bookmark - pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) { - let params = match self.collect_host_params() { - Ok(p) => p, - Err(e) => { - self.mount_error(e); - return; - } + pub(super) fn save_bookmark(&mut self, form_tab: FormTab, name: String, save_password: bool) { + let params = match form_tab { + FormTab::Remote => match self.collect_remote_host_params() { + Ok(p) => p, + Err(e) => { + self.mount_error(e); + return; + } + }, + FormTab::HostBridge => match self.collect_host_bridge_params() { + Ok(HostBridgeParams::Remote(protocol, params)) => FileTransferParams { + protocol, + params, + remote_path: None, + local_path: None, + }, + Ok(HostBridgeParams::Localhost(_)) => { + self.mount_error("You cannot save a localhost bookmark"); + return; + } + Err(e) => { + self.mount_error(e); + return; + } + }, }; + if let Some(bookmarks_cli) = self.bookmarks_client_mut() { bookmarks_cli.add_bookmark(name.clone(), params, save_password); // Save bookmarks @@ -73,13 +96,16 @@ impl AuthActivity { } /// Load selected recent (at index) to input fields - pub(super) fn load_recent(&mut self, idx: usize) { + pub(super) fn load_recent(&mut self, form_tab: FormTab, idx: usize) { if let Some(client) = self.bookmarks_client() { // Iterate over bookmarks if let Some(key) = self.recents_list.get(idx) { if let Some(bookmark) = client.get_recent(key) { // Load parameters - self.load_bookmark_into_gui(bookmark); + match form_tab { + FormTab::Remote => self.load_remote_bookmark_into_gui(bookmark), + FormTab::HostBridge => self.load_host_bridge_bookmark_into_gui(bookmark), + } } } } @@ -87,7 +113,7 @@ impl AuthActivity { /// Save current input fields as a "recent" pub(super) fn save_recent(&mut self) { - let params = match self.collect_host_params() { + let params = match self.collect_remote_host_params() { Ok(p) => p, Err(e) => { self.mount_error(e); @@ -147,73 +173,125 @@ impl AuthActivity { } /// Load bookmark data into the gui components - fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) { + fn load_host_bridge_bookmark_into_gui(&mut self, bookmark: FileTransferParams) { // Load parameters into components - self.protocol = bookmark.protocol; - self.mount_protocol(bookmark.protocol); + self.host_bridge_protocol = HostBridgeProtocol::Remote(bookmark.protocol); + self.mount_host_bridge_protocol(self.host_bridge_protocol); self.mount_remote_directory( + FormTab::HostBridge, bookmark .remote_path .map(|x| x.to_string_lossy().to_string()) .unwrap_or_default(), ); self.mount_local_directory( + FormTab::HostBridge, bookmark .local_path .map(|x| x.to_string_lossy().to_string()) .unwrap_or_default(), ); match bookmark.params { - ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params), - ProtocolParams::Kube(params) => self.load_bookmark_kube_into_gui(params), + ProtocolParams::AwsS3(params) => { + self.load_bookmark_s3_into_gui(FormTab::HostBridge, params) + } + ProtocolParams::Kube(params) => { + self.load_bookmark_kube_into_gui(FormTab::HostBridge, params) + } - ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params), - ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(params), - ProtocolParams::WebDAV(params) => self.load_bookmark_webdav_into_gui(params), + ProtocolParams::Generic(params) => { + self.load_bookmark_generic_into_gui(FormTab::HostBridge, params) + } + ProtocolParams::Smb(params) => { + self.load_bookmark_smb_into_gui(FormTab::HostBridge, params) + } + ProtocolParams::WebDAV(params) => { + self.load_bookmark_webdav_into_gui(FormTab::HostBridge, params) + } } } - fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) { - self.mount_address(params.address.as_str()); - self.mount_port(params.port); - self.mount_username(params.username.as_deref().unwrap_or("")); - self.mount_password(params.password.as_deref().unwrap_or("")); + /// Load bookmark data into the gui components + fn load_remote_bookmark_into_gui(&mut self, bookmark: FileTransferParams) { + // Load parameters into components + self.remote_protocol = bookmark.protocol; + self.mount_remote_protocol(bookmark.protocol); + self.mount_remote_directory( + FormTab::Remote, + bookmark + .remote_path + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or_default(), + ); + self.mount_local_directory( + FormTab::Remote, + bookmark + .local_path + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or_default(), + ); + match bookmark.params { + ProtocolParams::AwsS3(params) => { + self.load_bookmark_s3_into_gui(FormTab::Remote, params) + } + ProtocolParams::Kube(params) => { + self.load_bookmark_kube_into_gui(FormTab::Remote, params) + } + + ProtocolParams::Generic(params) => { + self.load_bookmark_generic_into_gui(FormTab::Remote, params) + } + ProtocolParams::Smb(params) => self.load_bookmark_smb_into_gui(FormTab::Remote, params), + ProtocolParams::WebDAV(params) => { + self.load_bookmark_webdav_into_gui(FormTab::Remote, params) + } + } } - fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) { - self.mount_s3_bucket(params.bucket_name.as_str()); - self.mount_s3_region(params.region.as_deref().unwrap_or("")); - self.mount_s3_endpoint(params.endpoint.as_deref().unwrap_or("")); - self.mount_s3_profile(params.profile.as_deref().unwrap_or("")); - self.mount_s3_access_key(params.access_key.as_deref().unwrap_or("")); - self.mount_s3_secret_access_key(params.secret_access_key.as_deref().unwrap_or("")); - self.mount_s3_security_token(params.security_token.as_deref().unwrap_or("")); - self.mount_s3_session_token(params.session_token.as_deref().unwrap_or("")); - self.mount_s3_new_path_style(params.new_path_style); + fn load_bookmark_generic_into_gui(&mut self, form_tab: FormTab, params: GenericProtocolParams) { + self.mount_address(form_tab, params.address.as_str()); + self.mount_port(form_tab, params.port); + self.mount_username(form_tab, params.username.as_deref().unwrap_or("")); + self.mount_password(form_tab, params.password.as_deref().unwrap_or("")); } - fn load_bookmark_kube_into_gui(&mut self, params: KubeProtocolParams) { - self.mount_kube_cluster_url(params.cluster_url.as_deref().unwrap_or("")); - self.mount_kube_namespace(params.namespace.as_deref().unwrap_or("")); - self.mount_kube_client_cert(params.client_cert.as_deref().unwrap_or("")); - self.mount_kube_client_key(params.client_key.as_deref().unwrap_or("")); - self.mount_kube_username(params.username.as_deref().unwrap_or("")); + fn load_bookmark_s3_into_gui(&mut self, form_tab: FormTab, params: AwsS3Params) { + self.mount_s3_bucket(form_tab, params.bucket_name.as_str()); + self.mount_s3_region(form_tab, params.region.as_deref().unwrap_or("")); + self.mount_s3_endpoint(form_tab, params.endpoint.as_deref().unwrap_or("")); + self.mount_s3_profile(form_tab, params.profile.as_deref().unwrap_or("")); + self.mount_s3_access_key(form_tab, params.access_key.as_deref().unwrap_or("")); + self.mount_s3_secret_access_key( + form_tab, + params.secret_access_key.as_deref().unwrap_or(""), + ); + self.mount_s3_security_token(form_tab, params.security_token.as_deref().unwrap_or("")); + self.mount_s3_session_token(form_tab, params.session_token.as_deref().unwrap_or("")); + self.mount_s3_new_path_style(form_tab, params.new_path_style); } - fn load_bookmark_smb_into_gui(&mut self, params: SmbParams) { - self.mount_address(params.address.as_str()); + fn load_bookmark_kube_into_gui(&mut self, form_tab: FormTab, params: KubeProtocolParams) { + self.mount_kube_cluster_url(form_tab, params.cluster_url.as_deref().unwrap_or("")); + self.mount_kube_namespace(form_tab, params.namespace.as_deref().unwrap_or("")); + self.mount_kube_client_cert(form_tab, params.client_cert.as_deref().unwrap_or("")); + self.mount_kube_client_key(form_tab, params.client_key.as_deref().unwrap_or("")); + self.mount_kube_username(form_tab, params.username.as_deref().unwrap_or("")); + } + + fn load_bookmark_smb_into_gui(&mut self, form_tab: FormTab, params: SmbParams) { + self.mount_address(form_tab, params.address.as_str()); #[cfg(unix)] - self.mount_port(params.port); - self.mount_username(params.username.as_deref().unwrap_or("")); - self.mount_password(params.password.as_deref().unwrap_or("")); - self.mount_smb_share(¶ms.share); + self.mount_port(form_tab, params.port); + self.mount_username(form_tab, params.username.as_deref().unwrap_or("")); + self.mount_password(form_tab, params.password.as_deref().unwrap_or("")); + self.mount_smb_share(form_tab, ¶ms.share); #[cfg(unix)] - self.mount_smb_workgroup(params.workgroup.as_deref().unwrap_or("")); + self.mount_smb_workgroup(form_tab, params.workgroup.as_deref().unwrap_or("")); } - fn load_bookmark_webdav_into_gui(&mut self, params: WebDAVProtocolParams) { - self.mount_webdav_uri(¶ms.uri); - self.mount_username(¶ms.username); - self.mount_password(¶ms.password); + fn load_bookmark_webdav_into_gui(&mut self, form_tab: FormTab, params: WebDAVProtocolParams) { + self.mount_webdav_uri(form_tab, ¶ms.uri); + self.mount_username(form_tab, ¶ms.username); + self.mount_password(form_tab, ¶ms.password); } } diff --git a/src/ui/activities/auth/components/bookmarks.rs b/src/ui/activities/auth/components/bookmarks.rs index 00bbaf0..178e377 100644 --- a/src/ui/activities/auth/components/bookmarks.rs +++ b/src/ui/activities/auth/components/bookmarks.rs @@ -9,6 +9,7 @@ use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputT use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use super::{FormMsg, Msg, UiMsg}; +use crate::ui::activities::auth::FormTab; // -- bookmark list @@ -323,10 +324,11 @@ impl Component for DeleteRecentPopup { #[derive(MockComponent)] pub struct BookmarkSavePassword { component: Radio, + form_tab: FormTab, } impl BookmarkSavePassword { - pub fn new(color: Color) -> Self { + pub fn new(form_tab: FormTab, color: Color) -> Self { Self { component: Radio::default() .borders( @@ -340,6 +342,7 @@ impl BookmarkSavePassword { .rewind(true) .foreground(color) .title("Save secrets?", Alignment::Center), + form_tab, } } } @@ -364,7 +367,7 @@ impl Component for BookmarkSavePassword { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::SaveBookmark)), + }) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))), Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { Some(Msg::Ui(UiMsg::SaveBookmarkPasswordBlur)) } @@ -378,10 +381,11 @@ impl Component for BookmarkSavePassword { #[derive(MockComponent)] pub struct BookmarkName { component: Input, + form_tab: FormTab, } impl BookmarkName { - pub fn new(color: Color) -> Self { + pub fn new(form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -393,6 +397,7 @@ impl BookmarkName { .foreground(color) .title("Bookmark name", Alignment::Left) .input_type(InputType::Text), + form_tab, } } } @@ -447,7 +452,7 @@ impl Component for BookmarkName { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Msg::Form(FormMsg::SaveBookmark)), + }) => Some(Msg::Form(FormMsg::SaveBookmark(self.form_tab))), Event::Keyboard(KeyEvent { code: Key::Down, .. }) => Some(Msg::Ui(UiMsg::BookmarkNameBlur)), diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs index d37e5e9..aafdbc0 100644 --- a/src/ui/activities/auth/components/form.rs +++ b/src/ui/activities/auth/components/form.rs @@ -10,18 +10,24 @@ use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use super::{FileTransferProtocol, FormMsg, Msg, UiMsg}; use crate::ui::activities::auth::{ - RADIO_PROTOCOL_FTP, RADIO_PROTOCOL_FTPS, RADIO_PROTOCOL_KUBE, RADIO_PROTOCOL_S3, - RADIO_PROTOCOL_SCP, RADIO_PROTOCOL_SFTP, RADIO_PROTOCOL_SMB, RADIO_PROTOCOL_WEBDAV, + FormTab, HostBridgeProtocol, UiAuthFormMsg, HOST_BRIDGE_RADIO_PROTOCOL_FTP, + HOST_BRIDGE_RADIO_PROTOCOL_FTPS, HOST_BRIDGE_RADIO_PROTOCOL_KUBE, + HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST, HOST_BRIDGE_RADIO_PROTOCOL_S3, + HOST_BRIDGE_RADIO_PROTOCOL_SCP, HOST_BRIDGE_RADIO_PROTOCOL_SFTP, + HOST_BRIDGE_RADIO_PROTOCOL_SMB, HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV, REMOTE_RADIO_PROTOCOL_FTP, + REMOTE_RADIO_PROTOCOL_FTPS, REMOTE_RADIO_PROTOCOL_KUBE, REMOTE_RADIO_PROTOCOL_S3, + REMOTE_RADIO_PROTOCOL_SCP, REMOTE_RADIO_PROTOCOL_SFTP, REMOTE_RADIO_PROTOCOL_SMB, + REMOTE_RADIO_PROTOCOL_WEBDAV, }; // -- protocol #[derive(MockComponent)] -pub struct ProtocolRadio { +pub struct RemoteProtocolRadio { component: Radio, } -impl ProtocolRadio { +impl RemoteProtocolRadio { pub fn new(default_protocol: FileTransferProtocol, color: Color) -> Self { Self { component: Radio::default() @@ -45,13 +51,13 @@ impl ProtocolRadio { /// Convert radio index for protocol into a `FileTransferProtocol` fn protocol_opt_to_enum(protocol: usize) -> FileTransferProtocol { match protocol { - RADIO_PROTOCOL_SCP => FileTransferProtocol::Scp, - RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false), - RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true), - RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3, - RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb, - RADIO_PROTOCOL_KUBE => FileTransferProtocol::Kube, - RADIO_PROTOCOL_WEBDAV => FileTransferProtocol::WebDAV, + REMOTE_RADIO_PROTOCOL_SCP => FileTransferProtocol::Scp, + REMOTE_RADIO_PROTOCOL_FTP => FileTransferProtocol::Ftp(false), + REMOTE_RADIO_PROTOCOL_FTPS => FileTransferProtocol::Ftp(true), + REMOTE_RADIO_PROTOCOL_S3 => FileTransferProtocol::AwsS3, + REMOTE_RADIO_PROTOCOL_SMB => FileTransferProtocol::Smb, + REMOTE_RADIO_PROTOCOL_KUBE => FileTransferProtocol::Kube, + REMOTE_RADIO_PROTOCOL_WEBDAV => FileTransferProtocol::WebDAV, _ => FileTransferProtocol::Sftp, } } @@ -59,19 +65,19 @@ impl ProtocolRadio { /// Convert `FileTransferProtocol` enum into radio group index fn protocol_enum_to_opt(protocol: FileTransferProtocol) -> usize { match protocol { - FileTransferProtocol::Sftp => RADIO_PROTOCOL_SFTP, - FileTransferProtocol::Scp => RADIO_PROTOCOL_SCP, - FileTransferProtocol::Ftp(false) => RADIO_PROTOCOL_FTP, - FileTransferProtocol::Ftp(true) => RADIO_PROTOCOL_FTPS, - FileTransferProtocol::AwsS3 => RADIO_PROTOCOL_S3, - FileTransferProtocol::Kube => RADIO_PROTOCOL_KUBE, - FileTransferProtocol::Smb => RADIO_PROTOCOL_SMB, - FileTransferProtocol::WebDAV => RADIO_PROTOCOL_WEBDAV, + FileTransferProtocol::Sftp => REMOTE_RADIO_PROTOCOL_SFTP, + FileTransferProtocol::Scp => REMOTE_RADIO_PROTOCOL_SCP, + FileTransferProtocol::Ftp(false) => REMOTE_RADIO_PROTOCOL_FTP, + FileTransferProtocol::Ftp(true) => REMOTE_RADIO_PROTOCOL_FTPS, + FileTransferProtocol::AwsS3 => REMOTE_RADIO_PROTOCOL_S3, + FileTransferProtocol::Kube => REMOTE_RADIO_PROTOCOL_KUBE, + FileTransferProtocol::Smb => REMOTE_RADIO_PROTOCOL_SMB, + FileTransferProtocol::WebDAV => REMOTE_RADIO_PROTOCOL_WEBDAV, } } } -impl Component for ProtocolRadio { +impl Component for RemoteProtocolRadio { fn on(&mut self, ev: Event) -> Option { let result = match ev { Event::Keyboard(KeyEvent { @@ -85,18 +91,156 @@ impl Component for ProtocolRadio { }) => return Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => return Some(Msg::Ui(UiMsg::ProtocolBlurDown)), + }) => return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurDown))), Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - return Some(Msg::Ui(UiMsg::ProtocolBlurUp)) + return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ProtocolBlurUp))) } Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - return Some(Msg::Ui(UiMsg::ParamsFormBlur)) + return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur))) } + Event::Keyboard(KeyEvent { + code: Key::BackTab, .. + }) => return Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ChangeFormTab))), _ => return None, }; match result { CmdResult::Changed(State::One(StateValue::Usize(choice))) => Some(Msg::Form( - FormMsg::ProtocolChanged(Self::protocol_opt_to_enum(choice)), + FormMsg::RemoteProtocolChanged(Self::protocol_opt_to_enum(choice)), + )), + _ => Some(Msg::None), + } + } +} + +#[derive(MockComponent)] +pub struct HostBridgeProtocolRadio { + component: Radio, +} + +impl HostBridgeProtocolRadio { + pub fn new(protocol: HostBridgeProtocol, color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .choices(if cfg!(smb) { + &[ + "Localhost", + "SFTP", + "SCP", + "FTP", + "FTPS", + "S3", + "Kube", + "WebDAV", + "SMB", + ] + } else { + &[ + "Localhost", + "SFTP", + "SCP", + "FTP", + "FTPS", + "S3", + "Kube", + "WebDAV", + ] + }) + .foreground(color) + .rewind(true) + .title("Host type", Alignment::Left) + .value(Self::protocol_to_opt(protocol)), + } + } + + fn protocol_to_opt(protocol: HostBridgeProtocol) -> usize { + match protocol { + HostBridgeProtocol::Localhost => HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST, + HostBridgeProtocol::Remote(FileTransferProtocol::Sftp) => { + HOST_BRIDGE_RADIO_PROTOCOL_SFTP + } + HostBridgeProtocol::Remote(FileTransferProtocol::Scp) => HOST_BRIDGE_RADIO_PROTOCOL_SCP, + HostBridgeProtocol::Remote(FileTransferProtocol::Ftp(false)) => { + HOST_BRIDGE_RADIO_PROTOCOL_FTP + } + HostBridgeProtocol::Remote(FileTransferProtocol::Ftp(true)) => { + HOST_BRIDGE_RADIO_PROTOCOL_FTPS + } + HostBridgeProtocol::Remote(FileTransferProtocol::AwsS3) => { + HOST_BRIDGE_RADIO_PROTOCOL_S3 + } + HostBridgeProtocol::Remote(FileTransferProtocol::Smb) => HOST_BRIDGE_RADIO_PROTOCOL_SMB, + HostBridgeProtocol::Remote(FileTransferProtocol::Kube) => { + HOST_BRIDGE_RADIO_PROTOCOL_KUBE + } + HostBridgeProtocol::Remote(FileTransferProtocol::WebDAV) => { + HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV + } + } + } + + /// Convert radio index for protocol into a `FileTransferProtocol` + fn protocol_opt_to_enum(protocol: usize) -> HostBridgeProtocol { + match protocol { + HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST => HostBridgeProtocol::Localhost, + HOST_BRIDGE_RADIO_PROTOCOL_SFTP => { + HostBridgeProtocol::Remote(FileTransferProtocol::Sftp) + } + HOST_BRIDGE_RADIO_PROTOCOL_SCP => HostBridgeProtocol::Remote(FileTransferProtocol::Scp), + HOST_BRIDGE_RADIO_PROTOCOL_FTP => { + HostBridgeProtocol::Remote(FileTransferProtocol::Ftp(false)) + } + HOST_BRIDGE_RADIO_PROTOCOL_FTPS => { + HostBridgeProtocol::Remote(FileTransferProtocol::Ftp(true)) + } + HOST_BRIDGE_RADIO_PROTOCOL_S3 => { + HostBridgeProtocol::Remote(FileTransferProtocol::AwsS3) + } + HOST_BRIDGE_RADIO_PROTOCOL_SMB => HostBridgeProtocol::Remote(FileTransferProtocol::Smb), + HOST_BRIDGE_RADIO_PROTOCOL_KUBE => { + HostBridgeProtocol::Remote(FileTransferProtocol::Kube) + } + HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV => { + HostBridgeProtocol::Remote(FileTransferProtocol::WebDAV) + } + _ => HostBridgeProtocol::Localhost, + } + } +} + +impl Component for HostBridgeProtocolRadio { + fn on(&mut self, ev: Event) -> Option { + let result = match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => self.perform(Cmd::Move(Direction::Left)), + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => self.perform(Cmd::Move(Direction::Right)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => return Some(Msg::Form(FormMsg::Connect)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurDown))), + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurUp))) + } + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { + return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur))) + } + Event::Keyboard(KeyEvent { + code: Key::BackTab, .. + }) => return Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ChangeFormTab))), + _ => return None, + }; + match result { + CmdResult::Changed(State::One(StateValue::Usize(choice))) => Some(Msg::Form( + FormMsg::HostBridgeProtocolChanged(Self::protocol_opt_to_enum(choice)), )), _ => Some(Msg::None), } @@ -108,10 +252,11 @@ impl Component for ProtocolRadio { #[derive(MockComponent)] pub struct InputRemoteDirectory { component: Input, + form_tab: FormTab, } impl InputRemoteDirectory { - pub fn new(remote_dir: &str, color: Color) -> Self { + pub fn new(remote_dir: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -124,18 +269,26 @@ impl InputRemoteDirectory { .title("Default remote working directory", Alignment::Left) .input_type(InputType::Text) .value(remote_dir), + form_tab, } } } impl Component for InputRemoteDirectory { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::RemoteDirectoryBlurDown), - Msg::Ui(UiMsg::RemoteDirectoryBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurDown)), + FormTab::HostBridge => { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurDown)) + } + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurUp)), + }; + + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -144,10 +297,11 @@ impl Component for InputRemoteDirectory { #[derive(MockComponent)] pub struct InputLocalDirectory { component: Input, + form_tab: FormTab, } impl InputLocalDirectory { - pub fn new(local_dir: &str, color: Color) -> Self { + pub fn new(local_dir: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -160,18 +314,25 @@ impl InputLocalDirectory { .title("Default local working directory", Alignment::Left) .input_type(InputType::Text) .value(local_dir), + form_tab, } } } impl Component for InputLocalDirectory { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::LocalDirectoryBlurDown), - Msg::Ui(UiMsg::LocalDirectoryBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurDown)), + FormTab::HostBridge => { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurDown)) + } + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -180,10 +341,11 @@ impl Component for InputLocalDirectory { #[derive(MockComponent)] pub struct InputAddress { component: Input, + form_tab: FormTab, } impl InputAddress { - pub fn new(host: &str, color: Color) -> Self { + pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -196,18 +358,23 @@ impl InputAddress { .title("Remote host", Alignment::Left) .input_type(InputType::Text) .value(host), + form_tab, } } } impl Component for InputAddress { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::AddressBlurDown), - Msg::Ui(UiMsg::AddressBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::AddressBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::AddressBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::AddressBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::AddressBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -216,10 +383,11 @@ impl Component for InputAddress { #[derive(MockComponent)] pub struct InputPort { component: Input, + form_tab: FormTab, } impl InputPort { - pub fn new(port: u16, color: Color) -> Self { + pub fn new(port: u16, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -233,18 +401,23 @@ impl InputPort { .input_len(5) .title("Port number", Alignment::Left) .value(port.to_string()), + form_tab, } } } impl Component for InputPort { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::PortBlurDown), - Msg::Ui(UiMsg::PortBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PortBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PortBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PortBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PortBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -253,10 +426,11 @@ impl Component for InputPort { #[derive(MockComponent)] pub struct InputUsername { component: Input, + form_tab: FormTab, } impl InputUsername { - pub fn new(username: &str, color: Color) -> Self { + pub fn new(username: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -269,18 +443,23 @@ impl InputUsername { .title("Username", Alignment::Left) .input_type(InputType::Text) .value(username), + form_tab, } } } impl Component for InputUsername { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::UsernameBlurDown), - Msg::Ui(UiMsg::UsernameBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::UsernameBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::UsernameBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -289,10 +468,11 @@ impl Component for InputUsername { #[derive(MockComponent)] pub struct InputPassword { component: Input, + form_tab: FormTab, } impl InputPassword { - pub fn new(password: &str, color: Color) -> Self { + pub fn new(password: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -304,18 +484,23 @@ impl InputPassword { .title("Password", Alignment::Left) .input_type(InputType::Password('*')) .value(password), + form_tab, } } } impl Component for InputPassword { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::PasswordBlurDown), - Msg::Ui(UiMsg::PasswordBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PasswordBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PasswordBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -324,10 +509,11 @@ impl Component for InputPassword { #[derive(MockComponent)] pub struct InputS3Bucket { component: Input, + form_tab: FormTab, } impl InputS3Bucket { - pub fn new(bucket: &str, color: Color) -> Self { + pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -340,18 +526,23 @@ impl InputS3Bucket { .title("Bucket name", Alignment::Left) .input_type(InputType::Text) .value(bucket), + form_tab, } } } impl Component for InputS3Bucket { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::S3BucketBlurDown), - Msg::Ui(UiMsg::S3BucketBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3BucketBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3BucketBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -360,10 +551,11 @@ impl Component for InputS3Bucket { #[derive(MockComponent)] pub struct InputS3Region { component: Input, + form_tab: FormTab, } impl InputS3Region { - pub fn new(region: &str, color: Color) -> Self { + pub fn new(region: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -376,18 +568,23 @@ impl InputS3Region { .title("Region", Alignment::Left) .input_type(InputType::Text) .value(region), + form_tab, } } } impl Component for InputS3Region { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::S3RegionBlurDown), - Msg::Ui(UiMsg::S3RegionBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3RegionBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3RegionBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -396,10 +593,11 @@ impl Component for InputS3Region { #[derive(MockComponent)] pub struct InputS3Endpoint { component: Input, + form_tab: FormTab, } impl InputS3Endpoint { - pub fn new(endpoint: &str, color: Color) -> Self { + pub fn new(endpoint: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -415,18 +613,23 @@ impl InputS3Endpoint { .title("Endpoint", Alignment::Left) .input_type(InputType::Text) .value(endpoint), + form_tab, } } } impl Component for InputS3Endpoint { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::S3EndpointBlurDown), - Msg::Ui(UiMsg::S3EndpointBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -435,10 +638,11 @@ impl Component for InputS3Endpoint { #[derive(MockComponent)] pub struct RadioS3NewPathStyle { component: Radio, + form_tab: FormTab, } impl RadioS3NewPathStyle { - pub fn new(new_path_style: bool, color: Color) -> Self { + pub fn new(new_path_style: bool, form_tab: FormTab, color: Color) -> Self { Self { component: Radio::default() .borders( @@ -451,6 +655,7 @@ impl RadioS3NewPathStyle { .rewind(true) .title("New path style", Alignment::Left) .value(usize::from(!new_path_style)), + form_tab, } } } @@ -475,12 +680,24 @@ impl Component for RadioS3NewPathStyle { }) => Some(Msg::Form(FormMsg::Connect)), Event::Keyboard(KeyEvent { code: Key::Down, .. - }) => Some(Msg::Ui(UiMsg::S3NewPathStyleBlurDown)), + }) => Some(if self.form_tab == FormTab::Remote { + Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3NewPathStyleBlurDown)) + } else { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3NewPathStyleBlurDown)) + }), Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - Some(Msg::Ui(UiMsg::S3NewPathStyleBlurUp)) + Some(if self.form_tab == FormTab::Remote { + Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3NewPathStyleBlurUp)) + } else { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3NewPathStyleBlurUp)) + }) } Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { - Some(Msg::Ui(UiMsg::ParamsFormBlur)) + Some(if self.form_tab == FormTab::Remote { + Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur)) + } else { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur)) + }) } _ => None, } @@ -492,10 +709,11 @@ impl Component for RadioS3NewPathStyle { #[derive(MockComponent)] pub struct InputS3Profile { component: Input, + form_tab: FormTab, } impl InputS3Profile { - pub fn new(profile: &str, color: Color) -> Self { + pub fn new(profile: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -508,18 +726,23 @@ impl InputS3Profile { .title("Profile", Alignment::Left) .input_type(InputType::Text) .value(profile), + form_tab, } } } impl Component for InputS3Profile { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::S3ProfileBlurDown), - Msg::Ui(UiMsg::S3ProfileBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -528,10 +751,11 @@ impl Component for InputS3Profile { #[derive(MockComponent)] pub struct InputS3AccessKey { component: Input, + form_tab: FormTab, } impl InputS3AccessKey { - pub fn new(access_key: &str, color: Color) -> Self { + pub fn new(access_key: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -544,28 +768,34 @@ impl InputS3AccessKey { .title("Access key", Alignment::Left) .input_type(InputType::Text) .value(access_key), + form_tab, } } } impl Component for InputS3AccessKey { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::S3AccessKeyBlurDown), - Msg::Ui(UiMsg::S3AccessKeyBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } #[derive(MockComponent)] pub struct InputS3SecretAccessKey { component: Input, + form_tab: FormTab, } impl InputS3SecretAccessKey { - pub fn new(secret_access_key: &str, color: Color) -> Self { + pub fn new(secret_access_key: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -577,28 +807,38 @@ impl InputS3SecretAccessKey { .title("Secret access key", Alignment::Left) .input_type(InputType::Password('*')) .value(secret_access_key), + form_tab, } } } impl Component for InputS3SecretAccessKey { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::S3SecretAccessKeyBlurDown), - Msg::Ui(UiMsg::S3SecretAccessKeyBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurDown)), + FormTab::HostBridge => { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurDown)) + } + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurUp)), + FormTab::HostBridge => { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurUp)) + } + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } #[derive(MockComponent)] pub struct InputS3SecurityToken { component: Input, + form_tab: FormTab, } impl InputS3SecurityToken { - pub fn new(security_token: &str, color: Color) -> Self { + pub fn new(security_token: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -610,28 +850,36 @@ impl InputS3SecurityToken { .title("Security token", Alignment::Left) .input_type(InputType::Password('*')) .value(security_token), + form_tab, } } } impl Component for InputS3SecurityToken { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::S3SecurityTokenBlurDown), - Msg::Ui(UiMsg::S3SecurityTokenBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurDown)), + FormTab::HostBridge => { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurDown)) + } + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } #[derive(MockComponent)] pub struct InputS3SessionToken { component: Input, + form_tab: FormTab, } impl InputS3SessionToken { - pub fn new(session_token: &str, color: Color) -> Self { + pub fn new(session_token: &str, form_tab: FormTab, color: Color) -> Self { Self { component: Input::default() .borders( @@ -643,18 +891,365 @@ impl InputS3SessionToken { .title("Session token", Alignment::Left) .input_type(InputType::Password('*')) .value(session_token), + form_tab, } } } impl Component for InputS3SessionToken { fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::S3SessionTokenBlurDown), - Msg::Ui(UiMsg::S3SessionTokenBlurUp), - ) + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurDown)), + FormTab::HostBridge => { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurDown)) + } + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) + } +} + +#[derive(MockComponent)] +pub struct InputSmbShare { + component: Input, + form_tab: FormTab, +} + +impl InputSmbShare { + pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Share", Alignment::Left) + .input_type(InputType::Text) + .value(host), + form_tab, + } + } +} + +impl Component for InputSmbShare { + fn on(&mut self, ev: Event) -> Option { + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbShareBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbShareBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) + } +} + +#[cfg(unix)] +#[derive(MockComponent)] +pub struct InputSmbWorkgroup { + component: Input, + form_tab: FormTab, +} + +#[cfg(unix)] +impl InputSmbWorkgroup { + pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .title("Workgroup", Alignment::Left) + .input_type(InputType::Text) + .value(host), + form_tab, + } + } +} + +#[cfg(unix)] +impl Component for InputSmbWorkgroup { + fn on(&mut self, ev: Event) -> Option { + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) + } +} + +#[derive(MockComponent)] +pub struct InputWebDAVUri { + component: Input, + form_tab: FormTab, +} + +impl InputWebDAVUri { + pub fn new(host: &str, form_tab: FormTab, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder( + "http://localhost:8080", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("HTTP url", Alignment::Left) + .input_type(InputType::Text) + .value(host), + form_tab, + } + } +} + +impl Component for InputWebDAVUri { + fn on(&mut self, ev: Event) -> Option { + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) + } +} + +// kube + +#[derive(MockComponent)] +pub struct InputKubeNamespace { + component: Input, + form_tab: FormTab, +} + +impl InputKubeNamespace { + pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("namespace", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Pod namespace (optional)", Alignment::Left) + .input_type(InputType::Text) + .value(bucket), + form_tab, + } + } +} + +impl Component for InputKubeNamespace { + fn on(&mut self, ev: Event) -> Option { + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) + } +} + +#[derive(MockComponent)] +pub struct InputKubeClusterUrl { + component: Input, + form_tab: FormTab, +} + +impl InputKubeClusterUrl { + pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder( + "cluster url", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Kube cluster url (optional)", Alignment::Left) + .input_type(InputType::Text) + .value(bucket), + form_tab, + } + } +} + +impl Component for InputKubeClusterUrl { + fn on(&mut self, ev: Event) -> Option { + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurDown)), + FormTab::HostBridge => { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurDown)) + } + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) + } +} + +#[derive(MockComponent)] +pub struct InputKubeUsername { + component: Input, + form_tab: FormTab, +} + +impl InputKubeUsername { + pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("username", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Kube username (optional)", Alignment::Left) + .input_type(InputType::Text) + .value(bucket), + form_tab, + } + } +} + +impl Component for InputKubeUsername { + fn on(&mut self, ev: Event) -> Option { + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) + } +} + +#[derive(MockComponent)] +pub struct InputKubeClientCert { + component: Input, + form_tab: FormTab, +} + +impl InputKubeClientCert { + pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder( + "/home/user/.kube/client.crt", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Kube client cert path (optional)", Alignment::Left) + .input_type(InputType::Text) + .value(bucket), + form_tab, + } + } +} + +impl Component for InputKubeClientCert { + fn on(&mut self, ev: Event) -> Option { + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurDown)), + FormTab::HostBridge => { + Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurDown)) + } + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurUp)), + }; + + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) + } +} + +#[derive(MockComponent)] +pub struct InputKubeClientKey { + component: Input, + form_tab: FormTab, +} + +impl InputKubeClientKey { + pub fn new(bucket: &str, form_tab: FormTab, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder( + "/home/user/.kube/client.key", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Kube client key path (optional)", Alignment::Left) + .input_type(InputType::Text) + .value(bucket), + form_tab, + } + } +} + +impl Component for InputKubeClientKey { + fn on(&mut self, ev: Event) -> Option { + let on_key_down = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurDown)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurDown)), + }; + let on_key_up = match self.form_tab { + FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurUp)), + FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurUp)), + }; + let form_tab = self.form_tab; + handle_input_ev(self, ev, on_key_down, on_key_up, form_tab) } } @@ -663,6 +1258,7 @@ fn handle_input_ev( ev: Event, on_key_down: Msg, on_key_up: Msg, + form_tab: FormTab, ) -> Option { match ev { Event::Keyboard(KeyEvent { @@ -719,294 +1315,16 @@ fn handle_input_ev( code: Key::Down, .. }) => Some(on_key_down), Event::Keyboard(KeyEvent { code: Key::Up, .. }) => Some(on_key_up), - Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => Some(Msg::Ui(UiMsg::ParamsFormBlur)), + Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => match form_tab { + FormTab::HostBridge => Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur))), + FormTab::Remote => Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur))), + }, + Event::Keyboard(KeyEvent { + code: Key::BackTab, .. + }) => match form_tab { + FormTab::HostBridge => Some(Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::ChangeFormTab))), + FormTab::Remote => Some(Msg::Ui(UiMsg::Remote(UiAuthFormMsg::ChangeFormTab))), + }, _ => None, } } - -#[derive(MockComponent)] -pub struct InputSmbShare { - component: Input, -} - -impl InputSmbShare { - pub fn new(host: &str, color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .title("Share", Alignment::Left) - .input_type(InputType::Text) - .value(host), - } - } -} - -impl Component for InputSmbShare { - fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::SmbShareBlurDown), - Msg::Ui(UiMsg::SmbShareBlurUp), - ) - } -} - -#[cfg(unix)] -#[derive(MockComponent)] -pub struct InputSmbWorkgroup { - component: Input, -} - -#[cfg(unix)] -impl InputSmbWorkgroup { - pub fn new(host: &str, color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .title("Workgroup", Alignment::Left) - .input_type(InputType::Text) - .value(host), - } - } -} - -#[cfg(unix)] -impl Component for InputSmbWorkgroup { - fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::SmbWorkgroupDown), - Msg::Ui(UiMsg::SmbWorkgroupUp), - ) - } -} - -#[derive(MockComponent)] -pub struct InputWebDAVUri { - component: Input, -} - -impl InputWebDAVUri { - pub fn new(host: &str, color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .placeholder( - "http://localhost:8080", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("HTTP url", Alignment::Left) - .input_type(InputType::Text) - .value(host), - } - } -} - -impl Component for InputWebDAVUri { - fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::WebDAVUriBlurDown), - Msg::Ui(UiMsg::WebDAVUriBlurUp), - ) - } -} - -// kube - -#[derive(MockComponent)] -pub struct InputKubeNamespace { - component: Input, -} - -impl InputKubeNamespace { - pub fn new(bucket: &str, color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .placeholder("namespace", Style::default().fg(Color::Rgb(128, 128, 128))) - .title("Pod namespace (optional)", Alignment::Left) - .input_type(InputType::Text) - .value(bucket), - } - } -} - -impl Component for InputKubeNamespace { - fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::KubeNamespaceBlurDown), - Msg::Ui(UiMsg::KubeNamespaceBlurUp), - ) - } -} - -#[derive(MockComponent)] -pub struct InputKubeClusterUrl { - component: Input, -} - -impl InputKubeClusterUrl { - pub fn new(bucket: &str, color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .placeholder( - "cluster url", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("Kube cluster url (optional)", Alignment::Left) - .input_type(InputType::Text) - .value(bucket), - } - } -} - -impl Component for InputKubeClusterUrl { - fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::KubeClusterUrlBlurDown), - Msg::Ui(UiMsg::KubeClusterUrlBlurUp), - ) - } -} - -#[derive(MockComponent)] -pub struct InputKubeUsername { - component: Input, -} - -impl InputKubeUsername { - pub fn new(bucket: &str, color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .placeholder("username", Style::default().fg(Color::Rgb(128, 128, 128))) - .title("Kube username (optional)", Alignment::Left) - .input_type(InputType::Text) - .value(bucket), - } - } -} - -impl Component for InputKubeUsername { - fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::KubeUsernameBlurDown), - Msg::Ui(UiMsg::KubeUsernameBlurUp), - ) - } -} - -#[derive(MockComponent)] -pub struct InputKubeClientCert { - component: Input, -} - -impl InputKubeClientCert { - pub fn new(bucket: &str, color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .placeholder( - "/home/user/.kube/client.crt", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("Kube client cert path (optional)", Alignment::Left) - .input_type(InputType::Text) - .value(bucket), - } - } -} - -impl Component for InputKubeClientCert { - fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::KubeClientCertBlurDown), - Msg::Ui(UiMsg::KubeClientCertBlurUp), - ) - } -} - -#[derive(MockComponent)] -pub struct InputKubeClientKey { - component: Input, -} - -impl InputKubeClientKey { - pub fn new(bucket: &str, color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .placeholder( - "/home/user/.kube/client.key", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("Kube client key path (optional)", Alignment::Left) - .input_type(InputType::Text) - .value(bucket), - } - } -} - -impl Component for InputKubeClientKey { - fn on(&mut self, ev: Event) -> Option { - handle_input_ev( - self, - ev, - Msg::Ui(UiMsg::KubeClientKeyBlurDown), - Msg::Ui(UiMsg::KubeClientKeyBlurUp), - ) - } -} diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index 75c5ae5..353ca93 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -16,11 +16,12 @@ pub use bookmarks::{ #[cfg(unix)] pub use form::InputSmbWorkgroup; pub use form::{ - InputAddress, InputKubeClientCert, InputKubeClientKey, InputKubeClusterUrl, InputKubeNamespace, - InputKubeUsername, InputLocalDirectory, InputPassword, InputPort, InputRemoteDirectory, - InputS3AccessKey, InputS3Bucket, InputS3Endpoint, InputS3Profile, InputS3Region, - InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputSmbShare, - InputUsername, InputWebDAVUri, ProtocolRadio, RadioS3NewPathStyle, + HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey, + InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword, + InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint, + InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, + InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle, + RemoteProtocolRadio, }; pub use popup::{ ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup, diff --git a/src/ui/activities/auth/components/text.rs b/src/ui/activities/auth/components/text.rs index 4a47645..c56ae59 100644 --- a/src/ui/activities/auth/components/text.rs +++ b/src/ui/activities/auth/components/text.rs @@ -100,6 +100,8 @@ impl HelpFooter { TextSpan::from(" Change field "), TextSpan::from("").bold().fg(key_color), TextSpan::from(" Switch tab "), + TextSpan::from("").bold().fg(key_color), + TextSpan::from(" Switch form "), TextSpan::from("").bold().fg(key_color), TextSpan::from(" Submit form "), TextSpan::from("").bold().fg(key_color), diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index a9da774..423ad05 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -2,8 +2,11 @@ //! //! `auth_activity` is the module which implements the authentication activity -use super::{AuthActivity, FileTransferParams, FileTransferProtocol}; +use std::env; + +use super::{AuthActivity, FileTransferParams, FileTransferProtocol, FormTab, HostBridgeProtocol}; use crate::filetransfer::params::ProtocolParams; +use crate::filetransfer::HostBridgeParams; use crate::system::auto_update::{Release, Update, UpdateStatus}; use crate::system::notifications::Notification; @@ -36,24 +39,64 @@ impl AuthActivity { } /// Collect host params as `FileTransferParams` - pub(super) fn collect_host_params(&self) -> Result { - match self.protocol { - FileTransferProtocol::AwsS3 => self.collect_s3_host_params(), - FileTransferProtocol::Kube => self.collect_kube_host_params(), - FileTransferProtocol::Smb => self.collect_smb_host_params(), + pub(super) fn collect_host_bridge_params(&self) -> Result { + match self.host_bridge_protocol { + HostBridgeProtocol::Localhost => self.collect_localhost_host_params(), + HostBridgeProtocol::Remote(remote) => { + let transfer_params = match remote { + FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::HostBridge), + FileTransferProtocol::Kube => { + self.collect_kube_host_params(FormTab::HostBridge) + } + FileTransferProtocol::Smb => self.collect_smb_host_params(FormTab::HostBridge), + FileTransferProtocol::Ftp(_) + | FileTransferProtocol::Scp + | FileTransferProtocol::Sftp => { + self.collect_generic_host_params(remote, FormTab::HostBridge) + } + FileTransferProtocol::WebDAV => { + self.collect_webdav_host_params(FormTab::HostBridge) + } + }?; + + Ok(HostBridgeParams::Remote( + transfer_params.protocol, + transfer_params.params, + )) + } + } + } + + /// Collect host params as `FileTransferParams` + pub(super) fn collect_remote_host_params(&self) -> Result { + match self.remote_protocol { + FileTransferProtocol::AwsS3 => self.collect_s3_host_params(FormTab::Remote), + FileTransferProtocol::Kube => self.collect_kube_host_params(FormTab::Remote), + FileTransferProtocol::Smb => self.collect_smb_host_params(FormTab::Remote), FileTransferProtocol::Ftp(_) | FileTransferProtocol::Scp - | FileTransferProtocol::Sftp => self.collect_generic_host_params(self.protocol), - FileTransferProtocol::WebDAV => self.collect_webdav_host_params(), + | FileTransferProtocol::Sftp => { + self.collect_generic_host_params(self.remote_protocol, FormTab::Remote) + } + FileTransferProtocol::WebDAV => self.collect_webdav_host_params(FormTab::Remote), } } + fn collect_localhost_host_params(&self) -> Result { + let path = self + .get_input_local_directory(FormTab::HostBridge) + .unwrap_or_else(|| env::current_dir().unwrap_or_default()); + + Ok(HostBridgeParams::Localhost(path)) + } + /// Get input values from fields or return an error if fields are invalid to work as generic pub(super) fn collect_generic_host_params( &self, protocol: FileTransferProtocol, + form_tab: FormTab, ) -> Result { - let params = self.get_generic_params_input(); + let params = self.get_generic_params_input(form_tab); if params.address.is_empty() { return Err("Invalid host"); } @@ -63,39 +106,48 @@ impl AuthActivity { Ok(FileTransferParams { protocol, params: ProtocolParams::Generic(params), - local_path: self.get_input_local_directory(), - remote_path: self.get_input_remote_directory(), + local_path: self.get_input_local_directory(form_tab), + remote_path: self.get_input_remote_directory(form_tab), }) } /// Get input values from fields or return an error if fields are invalid to work as aws s3 - pub(super) fn collect_s3_host_params(&self) -> Result { - let params = self.get_s3_params_input(); + pub(super) fn collect_s3_host_params( + &self, + form_tab: FormTab, + ) -> Result { + let params = self.get_s3_params_input(form_tab); if params.bucket_name.is_empty() { return Err("Invalid bucket"); } Ok(FileTransferParams { protocol: FileTransferProtocol::AwsS3, params: ProtocolParams::AwsS3(params), - local_path: self.get_input_local_directory(), - remote_path: self.get_input_remote_directory(), + local_path: self.get_input_local_directory(form_tab), + remote_path: self.get_input_remote_directory(form_tab), }) } /// Get input values from fields or return an error if fields are invalid to work as aws s3 - pub(super) fn collect_kube_host_params(&self) -> Result { - let params = self.get_kube_params_input(); + pub(super) fn collect_kube_host_params( + &self, + form_tab: FormTab, + ) -> Result { + let params = self.get_kube_params_input(form_tab); Ok(FileTransferParams { protocol: FileTransferProtocol::Kube, params: ProtocolParams::Kube(params), - local_path: self.get_input_local_directory(), - remote_path: self.get_input_remote_directory(), + local_path: self.get_input_local_directory(form_tab), + remote_path: self.get_input_remote_directory(form_tab), }) } - pub(super) fn collect_smb_host_params(&self) -> Result { - let params = self.get_smb_params_input(); + pub(super) fn collect_smb_host_params( + &self, + form_tab: FormTab, + ) -> Result { + let params = self.get_smb_params_input(form_tab); if params.address.is_empty() { return Err("Invalid address"); } @@ -109,21 +161,24 @@ impl AuthActivity { Ok(FileTransferParams { protocol: FileTransferProtocol::Smb, params: ProtocolParams::Smb(params), - local_path: self.get_input_local_directory(), - remote_path: self.get_input_remote_directory(), + local_path: self.get_input_local_directory(form_tab), + remote_path: self.get_input_remote_directory(form_tab), }) } - pub(super) fn collect_webdav_host_params(&self) -> Result { - let params = self.get_webdav_params_input(); + pub(super) fn collect_webdav_host_params( + &self, + form_tab: FormTab, + ) -> Result { + let params = self.get_webdav_params_input(form_tab); if params.uri.is_empty() { return Err("Invalid URI"); } Ok(FileTransferParams { protocol: FileTransferProtocol::WebDAV, params: ProtocolParams::WebDAV(params), - local_path: self.get_input_local_directory(), - remote_path: self.get_input_remote_directory(), + local_path: self.get_input_local_directory(form_tab), + remote_path: self.get_input_remote_directory(form_tab), }) } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index cebf5ec..5fa25b7 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -23,20 +23,30 @@ use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; use crate::system::bookmarks_client::BookmarksClient; use crate::system::config_client::ConfigClient; -// radio -const RADIO_PROTOCOL_SFTP: usize = 0; -const RADIO_PROTOCOL_SCP: usize = 1; -const RADIO_PROTOCOL_FTP: usize = 2; -const RADIO_PROTOCOL_FTPS: usize = 3; -const RADIO_PROTOCOL_S3: usize = 4; -const RADIO_PROTOCOL_KUBE: usize = 5; -const RADIO_PROTOCOL_WEBDAV: usize = 6; -const RADIO_PROTOCOL_SMB: usize = 7; +// host bridge protocol radio +const HOST_BRIDGE_RADIO_PROTOCOL_LOCALHOST: usize = 0; +const HOST_BRIDGE_RADIO_PROTOCOL_SFTP: usize = 1; +const HOST_BRIDGE_RADIO_PROTOCOL_SCP: usize = 2; +const HOST_BRIDGE_RADIO_PROTOCOL_FTP: usize = 3; +const HOST_BRIDGE_RADIO_PROTOCOL_FTPS: usize = 4; +const HOST_BRIDGE_RADIO_PROTOCOL_S3: usize = 5; +const HOST_BRIDGE_RADIO_PROTOCOL_KUBE: usize = 6; +const HOST_BRIDGE_RADIO_PROTOCOL_WEBDAV: usize = 7; +const HOST_BRIDGE_RADIO_PROTOCOL_SMB: usize = 8; // Keep as last + +// remote protocol radio +const REMOTE_RADIO_PROTOCOL_SFTP: usize = 0; +const REMOTE_RADIO_PROTOCOL_SCP: usize = 1; +const REMOTE_RADIO_PROTOCOL_FTP: usize = 2; +const REMOTE_RADIO_PROTOCOL_FTPS: usize = 3; +const REMOTE_RADIO_PROTOCOL_S3: usize = 4; +const REMOTE_RADIO_PROTOCOL_KUBE: usize = 5; +const REMOTE_RADIO_PROTOCOL_WEBDAV: usize = 6; +const REMOTE_RADIO_PROTOCOL_SMB: usize = 7; // Keep as last // -- components #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum Id { - Address, BookmarkName, BookmarkSavePassword, BookmarksList, @@ -45,22 +55,33 @@ pub enum Id { ErrorPopup, GlobalListener, HelpFooter, + HostBridge(AuthFormId), InfoPopup, InstallUpdatePopup, Keybindings, + NewVersionChangelog, + NewVersionDisclaimer, + QuitPopup, + RecentsList, + Remote(AuthFormId), + Subtitle, + Title, + WaitPopup, + WindowSizeError, +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum AuthFormId { + Address, KubeNamespace, KubeClusterUrl, KubeUsername, KubeClientCert, KubeClientKey, LocalDirectory, - NewVersionChangelog, - NewVersionDisclaimer, Password, Port, Protocol, - QuitPopup, - RecentsList, RemoteDirectory, S3AccessKey, S3Bucket, @@ -74,23 +95,19 @@ pub enum Id { SmbShare, #[cfg(unix)] SmbWorkgroup, - Subtitle, - Title, Username, - WaitPopup, WebDAVUri, - WindowSizeError, } #[derive(Debug, Eq, PartialEq)] -pub enum Msg { +enum Msg { Form(FormMsg), Ui(UiMsg), None, } #[derive(Debug, PartialEq, Eq)] -pub enum FormMsg { +enum FormMsg { Connect, DeleteBookmark, DeleteRecent, @@ -98,15 +115,14 @@ pub enum FormMsg { InstallUpdate, LoadBookmark(usize), LoadRecent(usize), - ProtocolChanged(FileTransferProtocol), + HostBridgeProtocolChanged(HostBridgeProtocol), + RemoteProtocolChanged(FileTransferProtocol), Quit, - SaveBookmark, + SaveBookmark(FormTab), } #[derive(Debug, PartialEq, Eq)] pub enum UiMsg { - AddressBlurDown, - AddressBlurUp, BookmarksListBlur, BookmarksTabBlur, CloseDeleteBookmark, @@ -117,6 +133,25 @@ pub enum UiMsg { CloseKeybindingsPopup, CloseQuitPopup, CloseSaveBookmark, + HostBridge(UiAuthFormMsg), + RececentsListBlur, + Remote(UiAuthFormMsg), + BookmarkNameBlur, + SaveBookmarkPasswordBlur, + ShowDeleteBookmarkPopup, + ShowDeleteRecentPopup, + ShowKeybindingsPopup, + ShowQuitPopup, + ShowReleaseNotes, + ShowSaveBookmarkPopup, + WindowResized, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum UiAuthFormMsg { + AddressBlurDown, + AddressBlurUp, + ChangeFormTab, KubeNamespaceBlurDown, KubeNamespaceBlurUp, KubeClusterUrlBlurDown, @@ -136,7 +171,6 @@ pub enum UiMsg { PortBlurUp, ProtocolBlurDown, ProtocolBlurUp, - RececentsListBlur, RemoteDirectoryBlurDown, RemoteDirectoryBlurUp, S3AccessKeyBlurDown, @@ -163,19 +197,10 @@ pub enum UiMsg { SmbWorkgroupDown, #[cfg(unix)] SmbWorkgroupUp, - BookmarkNameBlur, - SaveBookmarkPasswordBlur, - ShowDeleteBookmarkPopup, - ShowDeleteRecentPopup, - ShowKeybindingsPopup, - ShowQuitPopup, - ShowReleaseNotes, - ShowSaveBookmarkPopup, UsernameBlurDown, UsernameBlurUp, WebDAVUriBlurDown, WebDAVUriBlurUp, - WindowResized, } /// Auth form input mask @@ -184,10 +209,23 @@ enum InputMask { Generic, AwsS3, Kube, + Localhost, Smb, WebDAV, } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum HostBridgeProtocol { + Localhost, + Remote(FileTransferProtocol), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FormTab { + HostBridge, + Remote, +} + // Store keys const STORE_KEY_LATEST_VERSION: &str = "AUTH_LATEST_VERSION"; const STORE_KEY_RELEASE_NOTES: &str = "AUTH_RELEASE_NOTES"; @@ -203,8 +241,11 @@ pub struct AuthActivity { exit_reason: Option, /// Should redraw ui redraw: bool, - /// Protocol - protocol: FileTransferProtocol, + /// Host bridge protocol + host_bridge_protocol: HostBridgeProtocol, + last_form_tab: FormTab, + /// Remote file transfer protocol + remote_protocol: FileTransferProtocol, context: Option, } @@ -220,9 +261,11 @@ impl AuthActivity { context: None, bookmarks_list: Vec::new(), exit_reason: None, + last_form_tab: FormTab::Remote, recents_list: Vec::new(), redraw: true, - protocol: FileTransferProtocol::Sftp, + host_bridge_protocol: HostBridgeProtocol::Localhost, + remote_protocol: FileTransferProtocol::Sftp, } } @@ -255,8 +298,23 @@ impl AuthActivity { } /// Get current input mask to show - fn input_mask(&self) -> InputMask { - match self.protocol { + fn remote_input_mask(&self) -> InputMask { + Self::file_transfer_protocol_input_mask(self.remote_protocol) + } + + /// Get current input mask to show + fn host_bridge_input_mask(&self) -> InputMask { + match self.host_bridge_protocol { + HostBridgeProtocol::Localhost => InputMask::Localhost, + HostBridgeProtocol::Remote(protocol) => { + Self::file_transfer_protocol_input_mask(protocol) + } + } + } + + /// Get input mask for protocol + fn file_transfer_protocol_input_mask(protocol: FileTransferProtocol) -> InputMask { + match protocol { FileTransferProtocol::AwsS3 => InputMask::AwsS3, FileTransferProtocol::Ftp(_) | FileTransferProtocol::Scp @@ -275,7 +333,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 diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index 79adde7..44b486a 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -4,7 +4,10 @@ use tuirealm::{State, StateValue}; -use super::{AuthActivity, ExitReason, FormMsg, Id, InputMask, Msg, UiMsg, Update}; +use super::{ + AuthActivity, AuthFormId, ExitReason, FormMsg, FormTab, HostBridgeProtocol, Id, InputMask, Msg, + UiAuthFormMsg, UiMsg, Update, +}; impl Update for AuthActivity { fn update(&mut self, msg: Option) -> Option { @@ -21,19 +24,26 @@ impl AuthActivity { fn update_form(&mut self, msg: FormMsg) -> Option { match msg { FormMsg::Connect => { - match self.collect_host_params() { - Err(err) => { - // mount error - self.mount_error(err); - } - Ok(params) => { - self.save_recent(); - // Set file transfer params to context - self.context_mut().set_ftparams(params); - // Set exit reason - self.exit_reason = Some(super::ExitReason::Connect); - } - } + let Ok(remote_params) = self.collect_remote_host_params() else { + // mount error + self.mount_error("Invalid remote params parameters"); + return None; + }; + + let Ok(host_bridge_params) = self.collect_host_bridge_params() else { + // mount error + self.mount_error("Invalid host bridge params parameters"); + return None; + }; + + self.save_recent(); + // Set file transfer params to context + self.context_mut().set_remote_params(remote_params); + // set host bridge params + self.context_mut() + .set_host_bridge_params(host_bridge_params); + // Set exit reason + self.exit_reason = Some(super::ExitReason::Connect); } FormMsg::DeleteBookmark => { if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) { @@ -62,50 +72,86 @@ impl AuthActivity { self.install_update(); } FormMsg::LoadBookmark(i) => { - self.load_bookmark(i); + self.load_bookmark(self.last_form_tab, i); // Give focus to input password (or to protocol if not generic) - assert!(self - .app - .active(match self.input_mask() { - InputMask::Generic => &Id::Password, - InputMask::Smb => &Id::Password, - InputMask::AwsS3 => &Id::S3Bucket, - InputMask::Kube => &Id::KubeNamespace, - InputMask::WebDAV => &Id::Password, - }) - .is_ok()); + let focus = match self.last_form_tab { + FormTab::Remote => match self.remote_input_mask() { + InputMask::Localhost => &Id::Remote(AuthFormId::LocalDirectory), + InputMask::Generic => &Id::Remote(AuthFormId::Password), + InputMask::Smb => &Id::Remote(AuthFormId::Password), + InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket), + InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace), + InputMask::WebDAV => &Id::Remote(AuthFormId::Password), + }, + FormTab::HostBridge => match self.host_bridge_input_mask() { + InputMask::Localhost => &Id::HostBridge(AuthFormId::LocalDirectory), + InputMask::Generic => &Id::HostBridge(AuthFormId::Password), + InputMask::Smb => &Id::HostBridge(AuthFormId::Password), + InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket), + InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace), + InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password), + }, + }; + + assert!(self.app.active(focus).is_ok()); } FormMsg::LoadRecent(i) => { - self.load_recent(i); + self.load_recent(self.last_form_tab, i); // Give focus to input password (or to protocol if not generic) - assert!(self - .app - .active(match self.input_mask() { - InputMask::Generic => &Id::Password, - InputMask::Smb => &Id::Password, - InputMask::AwsS3 => &Id::S3Bucket, - InputMask::Kube => &Id::KubeNamespace, - InputMask::WebDAV => &Id::Password, - }) - .is_ok()); + let focus = match self.last_form_tab { + FormTab::Remote => match self.remote_input_mask() { + InputMask::Localhost => &Id::Remote(AuthFormId::LocalDirectory), + InputMask::Generic => &Id::Remote(AuthFormId::Password), + InputMask::Smb => &Id::Remote(AuthFormId::Password), + InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket), + InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace), + InputMask::WebDAV => &Id::Remote(AuthFormId::Password), + }, + FormTab::HostBridge => match self.host_bridge_input_mask() { + InputMask::Localhost => &Id::HostBridge(AuthFormId::LocalDirectory), + InputMask::Generic => &Id::HostBridge(AuthFormId::Password), + InputMask::Smb => &Id::HostBridge(AuthFormId::Password), + InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket), + InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace), + InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password), + }, + }; + + assert!(self.app.active(focus).is_ok()); } - FormMsg::ProtocolChanged(protocol) => { - self.protocol = protocol; + FormMsg::HostBridgeProtocolChanged(protocol) => { + self.host_bridge_protocol = protocol; // Update port - let port: u16 = self.get_input_port(); + let port: u16 = self.get_input_port(FormTab::HostBridge); + if let HostBridgeProtocol::Remote(remote_protocol) = protocol { + if Self::is_port_standard(port) { + self.mount_port( + FormTab::HostBridge, + Self::get_default_port_for_protocol(remote_protocol), + ); + } + } + } + FormMsg::RemoteProtocolChanged(protocol) => { + self.remote_protocol = protocol; + // Update port + let port: u16 = self.get_input_port(FormTab::Remote); if Self::is_port_standard(port) { - self.mount_port(Self::get_default_port_for_protocol(protocol)); + self.mount_port( + FormTab::Remote, + Self::get_default_port_for_protocol(protocol), + ); } } FormMsg::Quit => { self.exit_reason = Some(ExitReason::Quit); } - FormMsg::SaveBookmark => { + FormMsg::SaveBookmark(form_tab) => { // get bookmark name let (name, save_password) = self.get_new_bookmark(); // Save bookmark if !name.is_empty() { - self.save_bookmark(name, save_password); + self.save_bookmark(form_tab, name, save_password); } // Umount popup self.umount_bookmark_save_dialog(); @@ -118,16 +164,30 @@ impl AuthActivity { fn update_ui(&mut self, msg: UiMsg) -> Option { match msg { - UiMsg::AddressBlurDown => { - let id = if cfg!(windows) && self.input_mask() == InputMask::Smb { - &Id::SmbShare + UiMsg::HostBridge(UiAuthFormMsg::AddressBlurDown) => { + let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb { + &Id::HostBridge(AuthFormId::SmbShare) } else { - &Id::Port + &Id::HostBridge(AuthFormId::Port) }; assert!(self.app.active(id).is_ok()); } - UiMsg::AddressBlurUp => { - assert!(self.app.active(&Id::Protocol).is_ok()); + UiMsg::Remote(UiAuthFormMsg::AddressBlurDown) => { + let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb { + &Id::Remote(AuthFormId::SmbShare) + } else { + &Id::Remote(AuthFormId::Port) + }; + assert!(self.app.active(id).is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::AddressBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Protocol)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::AddressBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok()); } UiMsg::BookmarksListBlur => { assert!(self.app.active(&Id::RecentsList).is_ok()); @@ -136,7 +196,21 @@ impl AuthActivity { assert!(self.app.active(&Id::BookmarkSavePassword).is_ok()); } UiMsg::BookmarksTabBlur => { - assert!(self.app.active(&Id::Protocol).is_ok()); + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Protocol)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::ChangeFormTab) => { + self.last_form_tab = FormTab::Remote; + assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::ChangeFormTab) => { + self.last_form_tab = FormTab::HostBridge; + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Protocol)) + .is_ok()); } UiMsg::CloseDeleteBookmark => { assert!(self.app.umount(&Id::DeleteBookmarkPopup).is_ok()); @@ -162,185 +236,554 @@ impl AuthActivity { assert!(self.app.umount(&Id::BookmarkName).is_ok()); assert!(self.app.umount(&Id::BookmarkSavePassword).is_ok()); } - UiMsg::LocalDirectoryBlurDown => { - assert!(self.app.active(&Id::Protocol).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Protocol)) + .is_ok()); } - UiMsg::LocalDirectoryBlurUp => { - assert!(self.app.active(&Id::RemoteDirectory).is_ok()); + UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurDown) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok()); } - UiMsg::ParamsFormBlur => { + UiMsg::HostBridge(UiAuthFormMsg::LocalDirectoryBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::RemoteDirectory)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::RemoteDirectory)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::ParamsFormBlur) => { assert!(self.app.active(&Id::BookmarksList).is_ok()); } - UiMsg::PasswordBlurDown => { + UiMsg::Remote(UiAuthFormMsg::ParamsFormBlur) => { + assert!(self.app.active(&Id::BookmarksList).is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurDown) => { assert!(self .app - .active(match self.input_mask() { - InputMask::Generic => &Id::RemoteDirectory, + .active(match self.host_bridge_input_mask() { + InputMask::Localhost => unreachable!(), + InputMask::Generic => &Id::HostBridge(AuthFormId::RemoteDirectory), #[cfg(unix)] - InputMask::Smb => &Id::SmbWorkgroup, + InputMask::Smb => &Id::HostBridge(AuthFormId::SmbWorkgroup), #[cfg(windows)] - InputMask::Smb => &Id::RemoteDirectory, - InputMask::AwsS3 => panic!("this shouldn't happen (password on s3)"), - InputMask::Kube => panic!("this shouldn't happen (password on kube)"), - InputMask::WebDAV => &Id::RemoteDirectory, + InputMask::Smb => &Id::HostBridge(AuthFormId::RemoteDirectory), + InputMask::AwsS3 => unreachable!("this shouldn't happen (password on s3)"), + InputMask::Kube => unreachable!("this shouldn't happen (password on kube)"), + InputMask::WebDAV => &Id::HostBridge(AuthFormId::RemoteDirectory), }) .is_ok()); } - UiMsg::PasswordBlurUp => { - assert!(self.app.active(&Id::Username).is_ok()); - } - UiMsg::PortBlurDown => { + UiMsg::Remote(UiAuthFormMsg::PasswordBlurDown) => { assert!(self .app - .active(match self.input_mask() { - InputMask::Generic => &Id::Username, - InputMask::Smb => &Id::SmbShare, - InputMask::AwsS3 | InputMask::Kube | InputMask::WebDAV => - panic!("this shouldn't happen (port on s3/kube/webdav)"), + .active(match self.remote_input_mask() { + InputMask::Localhost => unreachable!(), + InputMask::Generic => &Id::Remote(AuthFormId::RemoteDirectory), + #[cfg(unix)] + InputMask::Smb => &Id::Remote(AuthFormId::SmbWorkgroup), + #[cfg(windows)] + InputMask::Smb => &Id::Remote(AuthFormId::RemoteDirectory), + InputMask::AwsS3 => unreachable!("this shouldn't happen (password on s3)"), + InputMask::Kube => unreachable!("this shouldn't happen (password on kube)"), + InputMask::WebDAV => &Id::Remote(AuthFormId::RemoteDirectory), }) .is_ok()); } - UiMsg::PortBlurUp => { - assert!(self.app.active(&Id::Address).is_ok()); - } - UiMsg::ProtocolBlurDown => { + UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurUp) => { assert!(self .app - .active(match self.input_mask() { - InputMask::Generic => &Id::Address, - InputMask::Smb => &Id::Address, - InputMask::AwsS3 => &Id::S3Bucket, - InputMask::Kube => &Id::KubeNamespace, - InputMask::WebDAV => &Id::WebDAVUri, + .active(&Id::HostBridge(AuthFormId::Username)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::PasswordBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Username)).is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::PortBlurDown) => { + assert!(self + .app + .active(match self.host_bridge_input_mask() { + InputMask::Generic => &Id::HostBridge(AuthFormId::Username), + InputMask::Smb => &Id::HostBridge(AuthFormId::SmbShare), + InputMask::Localhost + | InputMask::AwsS3 + | InputMask::Kube + | InputMask::WebDAV => + unreachable!("this shouldn't happen (port on s3/kube/webdav)"), }) .is_ok()); } - UiMsg::ProtocolBlurUp => { - assert!(self.app.active(&Id::LocalDirectory).is_ok()); + UiMsg::Remote(UiAuthFormMsg::PortBlurDown) => { + assert!(self + .app + .active(match self.remote_input_mask() { + InputMask::Generic => &Id::Remote(AuthFormId::Username), + InputMask::Smb => &Id::Remote(AuthFormId::SmbShare), + InputMask::Localhost + | InputMask::AwsS3 + | InputMask::Kube + | InputMask::WebDAV => + unreachable!("this shouldn't happen (port on s3/kube/webdav)"), + }) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::PortBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Address)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::PortBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Address)).is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurDown) => { + assert!(self + .app + .active(match self.host_bridge_input_mask() { + InputMask::Localhost => &Id::HostBridge(AuthFormId::LocalDirectory), + InputMask::Generic => &Id::HostBridge(AuthFormId::Address), + InputMask::Smb => &Id::HostBridge(AuthFormId::Address), + InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3Bucket), + InputMask::Kube => &Id::HostBridge(AuthFormId::KubeNamespace), + InputMask::WebDAV => &Id::HostBridge(AuthFormId::WebDAVUri), + }) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::ProtocolBlurDown) => { + assert!(self + .app + .active(match self.remote_input_mask() { + InputMask::Localhost => &Id::Remote(AuthFormId::LocalDirectory), + InputMask::Generic => &Id::Remote(AuthFormId::Address), + InputMask::Smb => &Id::Remote(AuthFormId::Address), + InputMask::AwsS3 => &Id::Remote(AuthFormId::S3Bucket), + InputMask::Kube => &Id::Remote(AuthFormId::KubeNamespace), + InputMask::WebDAV => &Id::Remote(AuthFormId::WebDAVUri), + }) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::ProtocolBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::LocalDirectory)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::ProtocolBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::LocalDirectory)) + .is_ok()); } UiMsg::RececentsListBlur => { assert!(self.app.active(&Id::BookmarksList).is_ok()); } - UiMsg::RemoteDirectoryBlurDown => { - assert!(self.app.active(&Id::LocalDirectory).is_ok()); - } - UiMsg::RemoteDirectoryBlurUp => { + UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurDown) => { assert!(self .app - .active(match self.input_mask() { - InputMask::Generic => &Id::Password, + .active(&Id::HostBridge(AuthFormId::LocalDirectory)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::LocalDirectory)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::RemoteDirectoryBlurUp) => { + assert!(self + .app + .active(match self.host_bridge_input_mask() { + InputMask::Localhost => unreachable!(), + InputMask::Generic => &Id::HostBridge(AuthFormId::Password), #[cfg(unix)] - InputMask::Smb => &Id::SmbWorkgroup, + InputMask::Smb => &Id::HostBridge(AuthFormId::SmbWorkgroup), #[cfg(windows)] - InputMask::Smb => &Id::Password, - InputMask::Kube => &Id::KubeClientKey, - InputMask::AwsS3 => &Id::S3NewPathStyle, - InputMask::WebDAV => &Id::Password, + InputMask::Smb => &Id::HostBridge(AuthFormId::Password), + InputMask::Kube => &Id::HostBridge(AuthFormId::KubeClientKey), + InputMask::AwsS3 => &Id::HostBridge(AuthFormId::S3NewPathStyle), + InputMask::WebDAV => &Id::HostBridge(AuthFormId::Password), }) .is_ok()); } - UiMsg::S3BucketBlurDown => { - assert!(self.app.active(&Id::S3Region).is_ok()); + UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurUp) => { + assert!(self + .app + .active(match self.remote_input_mask() { + InputMask::Localhost => unreachable!(), + InputMask::Generic => &Id::Remote(AuthFormId::Password), + #[cfg(unix)] + InputMask::Smb => &Id::Remote(AuthFormId::SmbWorkgroup), + #[cfg(windows)] + InputMask::Smb => &Id::Remote(AuthFormId::Password), + InputMask::Kube => &Id::Remote(AuthFormId::KubeClientKey), + InputMask::AwsS3 => &Id::Remote(AuthFormId::S3NewPathStyle), + InputMask::WebDAV => &Id::Remote(AuthFormId::Password), + }) + .is_ok()); } - UiMsg::S3BucketBlurUp => { - assert!(self.app.active(&Id::Protocol).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3Region)) + .is_ok()); } - UiMsg::S3RegionBlurDown => { - assert!(self.app.active(&Id::S3Endpoint).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3BucketBlurDown) => { + assert!(self.app.active(&Id::Remote(AuthFormId::S3Region)).is_ok()); } - UiMsg::S3RegionBlurUp => { - assert!(self.app.active(&Id::S3Bucket).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Protocol)) + .is_ok()); } - UiMsg::S3EndpointBlurDown => { - assert!(self.app.active(&Id::S3Profile).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3BucketBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok()); } - UiMsg::S3EndpointBlurUp => { - assert!(self.app.active(&Id::S3Region).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3Endpoint)) + .is_ok()); } - UiMsg::S3ProfileBlurDown => { - assert!(self.app.active(&Id::S3AccessKey).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3RegionBlurDown) => { + assert!(self.app.active(&Id::Remote(AuthFormId::S3Endpoint)).is_ok()); } - UiMsg::S3ProfileBlurUp => { - assert!(self.app.active(&Id::S3Endpoint).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3Bucket)) + .is_ok()); } - UiMsg::S3AccessKeyBlurDown => { - assert!(self.app.active(&Id::S3SecretAccessKey).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3RegionBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::S3Bucket)).is_ok()); } - UiMsg::S3AccessKeyBlurUp => { - assert!(self.app.active(&Id::S3Profile).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3Profile)) + .is_ok()); } - UiMsg::S3SecretAccessKeyBlurDown => { - assert!(self.app.active(&Id::S3SecurityToken).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurDown) => { + assert!(self.app.active(&Id::Remote(AuthFormId::S3Profile)).is_ok()); } - UiMsg::S3SecretAccessKeyBlurUp => { - assert!(self.app.active(&Id::S3AccessKey).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3Region)) + .is_ok()); } - UiMsg::S3SecurityTokenBlurDown => { - assert!(self.app.active(&Id::S3SessionToken).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::S3Region)).is_ok()); } - UiMsg::S3SecurityTokenBlurUp => { - assert!(self.app.active(&Id::S3SecretAccessKey).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3AccessKey)) + .is_ok()); } - UiMsg::S3SessionTokenBlurDown => { - assert!(self.app.active(&Id::S3NewPathStyle).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3AccessKey)) + .is_ok()); } - UiMsg::S3SessionTokenBlurUp => { - assert!(self.app.active(&Id::S3SecurityToken).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3Endpoint)) + .is_ok()); } - UiMsg::S3NewPathStyleBlurDown => { - assert!(self.app.active(&Id::RemoteDirectory).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::S3Endpoint)).is_ok()); } - UiMsg::S3NewPathStyleBlurUp => { - assert!(self.app.active(&Id::S3SessionToken).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3SecretAccessKey)) + .is_ok()); } - UiMsg::KubeClientCertBlurDown => { - assert!(self.app.active(&Id::KubeClientKey).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3SecretAccessKey)) + .is_ok()); } - UiMsg::KubeClientCertBlurUp => { - assert!(self.app.active(&Id::KubeUsername).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3Profile)) + .is_ok()); } - UiMsg::KubeClientKeyBlurDown => { - assert!(self.app.active(&Id::RemoteDirectory).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::S3Profile)).is_ok()); } - UiMsg::KubeClientKeyBlurUp => { - assert!(self.app.active(&Id::KubeClientCert).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3SecurityToken)) + .is_ok()); } - UiMsg::KubeNamespaceBlurDown => { - assert!(self.app.active(&Id::KubeClusterUrl).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3SecurityToken)) + .is_ok()); } - UiMsg::KubeNamespaceBlurUp => { - assert!(self.app.active(&Id::Protocol).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3SecretAccessKeyBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3AccessKey)) + .is_ok()); } - UiMsg::KubeClusterUrlBlurDown => { - assert!(self.app.active(&Id::KubeUsername).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3AccessKey)) + .is_ok()); } - UiMsg::KubeClusterUrlBlurUp => { - assert!(self.app.active(&Id::KubeNamespace).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3SessionToken)) + .is_ok()); } - UiMsg::KubeUsernameBlurDown => { - assert!(self.app.active(&Id::KubeClientCert).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3SessionToken)) + .is_ok()); } - UiMsg::KubeUsernameBlurUp => { - assert!(self.app.active(&Id::KubeClusterUrl).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::S3SecurityTokenBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3SecretAccessKey)) + .is_ok()); } - UiMsg::SmbShareBlurDown => { - assert!(self.app.active(&Id::Username).is_ok()); + UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3SecretAccessKey)) + .is_ok()); } - UiMsg::SmbShareBlurUp => { - let id = if cfg!(windows) && self.input_mask() == InputMask::Smb { - &Id::Address + UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3NewPathStyle)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3NewPathStyle)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::S3SessionTokenBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3SecurityToken)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3SecurityToken)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::S3NewPathStyleBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::RemoteDirectory)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::S3NewPathStyleBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::RemoteDirectory)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::S3NewPathStyleBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::S3SessionToken)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::S3NewPathStyleBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::S3SessionToken)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::KubeClientKey)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::KubeClientKey)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeClientCertBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::KubeUsername)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::KubeUsername)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::RemoteDirectory)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::RemoteDirectory)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::KubeClientCert)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::KubeClientCert)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::KubeClusterUrl)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::KubeClusterUrl)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Protocol)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::KubeUsername)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::KubeUsername)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeClusterUrlBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::KubeNamespace)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::KubeNamespace)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::KubeClientCert)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::KubeClientCert)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::KubeClusterUrl)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurUp) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::KubeClusterUrl)) + .is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Username)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::SmbShareBlurDown) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Username)).is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurUp) => { + let id = if cfg!(windows) && self.host_bridge_input_mask() == InputMask::Smb { + &Id::HostBridge(AuthFormId::Address) } else { - &Id::Port + &Id::HostBridge(AuthFormId::Port) + }; + assert!(self.app.active(id).is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::SmbShareBlurUp) => { + let id = if cfg!(windows) && self.remote_input_mask() == InputMask::Smb { + &Id::Remote(AuthFormId::Address) + } else { + &Id::Remote(AuthFormId::Port) }; assert!(self.app.active(id).is_ok()); } #[cfg(unix)] - UiMsg::SmbWorkgroupDown => { - assert!(self.app.active(&Id::RemoteDirectory).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::RemoteDirectory)) + .is_ok()); } #[cfg(unix)] - UiMsg::SmbWorkgroupUp => { - assert!(self.app.active(&Id::Password).is_ok()); + UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupDown) => { + assert!(self + .app + .active(&Id::Remote(AuthFormId::RemoteDirectory)) + .is_ok()); + } + #[cfg(unix)] + UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Password)) + .is_ok()); + } + #[cfg(unix)] + UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Password)).is_ok()); } UiMsg::SaveBookmarkPasswordBlur => { assert!(self.app.active(&Id::BookmarkName).is_ok()); @@ -361,28 +804,60 @@ impl AuthActivity { self.mount_release_notes(); } UiMsg::ShowSaveBookmarkPopup => { - self.mount_bookmark_save_dialog(); + self.mount_bookmark_save_dialog(self.get_current_form_tab()); } - UiMsg::UsernameBlurDown => { - assert!(self.app.active(&Id::Password).is_ok()); - } - UiMsg::UsernameBlurUp => { + UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurDown) => { assert!(self .app - .active(match self.input_mask() { - InputMask::Generic => &Id::Port, - InputMask::Smb => &Id::SmbShare, - InputMask::Kube => panic!("this shouldn't happen (username on kube)"), - InputMask::AwsS3 => panic!("this shouldn't happen (username on s3)"), - InputMask::WebDAV => &Id::WebDAVUri, + .active(&Id::HostBridge(AuthFormId::Password)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::UsernameBlurDown) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Password)).is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurUp) => { + assert!(self + .app + .active(match self.host_bridge_input_mask() { + InputMask::Localhost => unreachable!(), + InputMask::Generic => &Id::HostBridge(AuthFormId::Port), + InputMask::Smb => &Id::HostBridge(AuthFormId::SmbShare), + InputMask::Kube => unreachable!("this shouldn't happen (username on kube)"), + InputMask::AwsS3 => unreachable!("this shouldn't happen (username on s3)"), + InputMask::WebDAV => &Id::HostBridge(AuthFormId::WebDAVUri), }) .is_ok()); } - UiMsg::WebDAVUriBlurDown => { - assert!(self.app.active(&Id::Username).is_ok()); + UiMsg::Remote(UiAuthFormMsg::UsernameBlurUp) => { + assert!(self + .app + .active(match self.remote_input_mask() { + InputMask::Localhost => unreachable!(), + InputMask::Generic => &Id::Remote(AuthFormId::Port), + InputMask::Smb => &Id::Remote(AuthFormId::SmbShare), + InputMask::Kube => unreachable!("this shouldn't happen (username on kube)"), + InputMask::AwsS3 => unreachable!("this shouldn't happen (username on s3)"), + InputMask::WebDAV => &Id::Remote(AuthFormId::WebDAVUri), + }) + .is_ok()); } - UiMsg::WebDAVUriBlurUp => { - assert!(self.app.active(&Id::Protocol).is_ok()); + UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurDown) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Username)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurDown) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Username)).is_ok()); + } + UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurUp) => { + assert!(self + .app + .active(&Id::HostBridge(AuthFormId::Protocol)) + .is_ok()); + } + UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurUp) => { + assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok()); } UiMsg::WindowResized => { self.redraw = true; diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index fc93707..5b8843e 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -11,7 +11,10 @@ use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::tui::widgets::Clear; use tuirealm::{State, StateValue, Sub, SubClause, SubEventClause}; -use super::{components, AuthActivity, Context, FileTransferProtocol, Id, InputMask}; +use super::{ + components, AuthActivity, AuthFormId, Context, FileTransferProtocol, FormTab, + HostBridgeProtocol, Id, InputMask, +}; use crate::filetransfer::params::{ AwsS3Params, GenericProtocolParams, KubeProtocolParams, ProtocolParams, SmbParams, WebDAVProtocolParams, @@ -42,34 +45,67 @@ impl AuthActivity { vec![] ) .is_ok()); - // Get default protocol - let default_protocol: FileTransferProtocol = self.context().config().get_default_protocol(); - // Auth form - self.mount_protocol(default_protocol); - self.mount_remote_directory(""); - self.mount_local_directory(""); - self.mount_address(""); - self.mount_port(Self::get_default_port_for_protocol(default_protocol)); - self.mount_username(""); - self.mount_password(""); - self.mount_s3_bucket(""); - self.mount_s3_profile(""); - self.mount_s3_region(""); - self.mount_s3_endpoint(""); - self.mount_s3_access_key(""); - self.mount_s3_secret_access_key(""); - self.mount_s3_security_token(""); - self.mount_s3_session_token(""); - self.mount_s3_new_path_style(false); - self.mount_kube_client_cert(""); - self.mount_kube_client_key(""); - self.mount_kube_cluster_url(""); - self.mount_kube_namespace(""); - self.mount_kube_username(""); - self.mount_smb_share(""); + + // Host bridge auth form + self.mount_host_bridge_protocol(HostBridgeProtocol::Localhost); + self.mount_remote_directory(FormTab::HostBridge, ""); + self.mount_local_directory(FormTab::HostBridge, ""); + self.mount_address(FormTab::HostBridge, ""); + self.mount_port(FormTab::HostBridge, 22); + self.mount_username(FormTab::HostBridge, ""); + self.mount_password(FormTab::HostBridge, ""); + self.mount_s3_bucket(FormTab::HostBridge, ""); + self.mount_s3_profile(FormTab::HostBridge, ""); + self.mount_s3_region(FormTab::HostBridge, ""); + self.mount_s3_endpoint(FormTab::HostBridge, ""); + self.mount_s3_access_key(FormTab::HostBridge, ""); + self.mount_s3_secret_access_key(FormTab::HostBridge, ""); + self.mount_s3_security_token(FormTab::HostBridge, ""); + self.mount_s3_session_token(FormTab::HostBridge, ""); + self.mount_s3_new_path_style(FormTab::HostBridge, false); + self.mount_kube_client_cert(FormTab::HostBridge, ""); + self.mount_kube_client_key(FormTab::HostBridge, ""); + self.mount_kube_cluster_url(FormTab::HostBridge, ""); + self.mount_kube_namespace(FormTab::HostBridge, ""); + self.mount_kube_username(FormTab::HostBridge, ""); + self.mount_smb_share(FormTab::HostBridge, ""); #[cfg(unix)] - self.mount_smb_workgroup(""); - self.mount_webdav_uri(""); + self.mount_smb_workgroup(FormTab::HostBridge, ""); + self.mount_webdav_uri(FormTab::HostBridge, ""); + + // Remote Auth form + // Get default protocol + let remote_default_protocol: FileTransferProtocol = + self.context().config().get_default_protocol(); + self.mount_remote_protocol(remote_default_protocol); + self.mount_remote_directory(FormTab::Remote, ""); + self.mount_local_directory(FormTab::Remote, ""); + self.mount_address(FormTab::Remote, ""); + self.mount_port( + FormTab::Remote, + Self::get_default_port_for_protocol(remote_default_protocol), + ); + self.mount_username(FormTab::Remote, ""); + self.mount_password(FormTab::Remote, ""); + self.mount_s3_bucket(FormTab::Remote, ""); + self.mount_s3_profile(FormTab::Remote, ""); + self.mount_s3_region(FormTab::Remote, ""); + self.mount_s3_endpoint(FormTab::Remote, ""); + self.mount_s3_access_key(FormTab::Remote, ""); + self.mount_s3_secret_access_key(FormTab::Remote, ""); + self.mount_s3_security_token(FormTab::Remote, ""); + self.mount_s3_session_token(FormTab::Remote, ""); + self.mount_s3_new_path_style(FormTab::Remote, false); + self.mount_kube_client_cert(FormTab::Remote, ""); + self.mount_kube_client_key(FormTab::Remote, ""); + self.mount_kube_cluster_url(FormTab::Remote, ""); + self.mount_kube_namespace(FormTab::Remote, ""); + self.mount_kube_username(FormTab::Remote, ""); + self.mount_smb_share(FormTab::Remote, ""); + #[cfg(unix)] + self.mount_smb_workgroup(FormTab::Remote, ""); + self.mount_webdav_uri(FormTab::Remote, ""); + // Version notice if let Some(version) = self .context() @@ -95,7 +131,7 @@ impl AuthActivity { // Global listener self.init_global_listener(); // Active protocol - assert!(self.app.active(&Id::Protocol).is_ok()); + assert!(self.app.active(&Id::Remote(AuthFormId::Protocol)).is_ok()); } /// Display view on canvas @@ -119,7 +155,7 @@ impl AuthActivity { .split(f.size()); // Footer self.app.view(&Id::HelpFooter, f, body[1]); - let auth_form_len = 7 + self.input_mask_size(); + let auth_form_len = 7 + self.max_input_mask_size(); let main_chunks = Layout::default() .direction(Direction::Vertical) .margin(1) @@ -135,79 +171,44 @@ impl AuthActivity { let auth_chunks = Layout::default() .constraints( [ - Constraint::Length(1), // h1 - Constraint::Length(1), // h2 - Constraint::Length(1), // Version - Constraint::Length(3), // protocol - Constraint::Length(self.input_mask_size()), // Input mask + Constraint::Length(1), // h1 + Constraint::Length(1), // h2 + Constraint::Length(1), // Version + Constraint::Length(self.max_input_mask_size()), // Input mask Constraint::Length(1), // Prevents last field to overflow ] .as_ref(), ) .direction(Direction::Vertical) .split(main_chunks[0]); - // Input mask chunks - let input_mask = Layout::default() - .constraints( - [ - Constraint::Length(3), // uri - Constraint::Length(3), // username - Constraint::Length(3), // password - Constraint::Length(3), // dir - ] - .as_ref(), - ) - .direction(Direction::Vertical) - .split(auth_chunks[4]); + // Create bookmark chunks let bookmark_chunks = Layout::default() .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .direction(Direction::Horizontal) + .spacing(2) .split(main_chunks[1]); // Render // Auth chunks self.app.view(&Id::Title, f, auth_chunks[0]); self.app.view(&Id::Subtitle, f, auth_chunks[1]); self.app.view(&Id::NewVersionDisclaimer, f, auth_chunks[2]); - self.app.view(&Id::Protocol, f, auth_chunks[3]); - // Render input mask - match self.input_mask() { - InputMask::AwsS3 => { - let view_ids = self.get_s3_view(); - self.app.view(&view_ids[0], f, input_mask[0]); - self.app.view(&view_ids[1], f, input_mask[1]); - self.app.view(&view_ids[2], f, input_mask[2]); - self.app.view(&view_ids[3], f, input_mask[3]); - } - InputMask::Generic => { - let view_ids = self.get_generic_params_view(); - self.app.view(&view_ids[0], f, input_mask[0]); - self.app.view(&view_ids[1], f, input_mask[1]); - self.app.view(&view_ids[2], f, input_mask[2]); - self.app.view(&view_ids[3], f, input_mask[3]); - } - InputMask::Kube => { - let view_ids = self.get_kube_view(); - self.app.view(&view_ids[0], f, input_mask[0]); - self.app.view(&view_ids[1], f, input_mask[1]); - self.app.view(&view_ids[2], f, input_mask[2]); - self.app.view(&view_ids[3], f, input_mask[3]); - } - InputMask::Smb => { - let view_ids = self.get_smb_view(); - self.app.view(&view_ids[0], f, input_mask[0]); - self.app.view(&view_ids[1], f, input_mask[1]); - self.app.view(&view_ids[2], f, input_mask[2]); - self.app.view(&view_ids[3], f, input_mask[3]); - } - InputMask::WebDAV => { - let view_ids = self.get_webdav_view(); - self.app.view(&view_ids[0], f, input_mask[0]); - self.app.view(&view_ids[1], f, input_mask[1]); - self.app.view(&view_ids[2], f, input_mask[2]); - self.app.view(&view_ids[3], f, input_mask[3]); - } - } + + // Render the host bridge and remote forms + let host_bridge_and_remote_chunks = Layout::default() + .constraints( + [ + Constraint::Percentage(50), // Host bridge + Constraint::Percentage(50), // Remote + ] + .as_ref(), + ) + .spacing(2) + .direction(Direction::Horizontal) + .split(auth_chunks[3]); + // Input mask + self.render_host_bridge_input_mask(f, host_bridge_and_remote_chunks[0]); + self.render_remote_input_mask(f, host_bridge_and_remote_chunks[1]); // Bookmark chunks self.app.view(&Id::BookmarksList, f, bookmark_chunks[0]); self.app.view(&Id::RecentsList, f, bookmark_chunks[1]); @@ -291,6 +292,159 @@ impl AuthActivity { // -- partials + fn render_host_bridge_input_mask( + &mut self, + f: &mut tuirealm::tui::Frame<'_>, + area: tuirealm::tui::layout::Rect, + ) { + let protocol_and_mask_chunks = Layout::default() + .constraints( + [ + Constraint::Length(3), // protocol + Constraint::Length(12), // Input mask + ] + .as_ref(), + ) + .direction(Direction::Vertical) + .split(area); + + self.app.view( + &Id::HostBridge(AuthFormId::Protocol), + f, + protocol_and_mask_chunks[0], + ); + + let input_mask = Layout::default() + .constraints( + [ + Constraint::Length(3), // uri + Constraint::Length(3), // username + Constraint::Length(3), // password + Constraint::Length(3), // dir + ] + .as_ref(), + ) + .direction(Direction::Vertical) + .split(protocol_and_mask_chunks[1]); + // Render input mask + match self.host_bridge_input_mask() { + InputMask::AwsS3 => { + let view_ids = self.get_host_bridge_s3_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + InputMask::Generic => { + let view_ids = self.get_host_bridge_generic_params_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + InputMask::Kube => { + let view_ids = self.get_host_bridge_kube_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + InputMask::Localhost => { + let view_ids = self.get_host_bridge_localhost_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + } + InputMask::Smb => { + let view_ids = self.get_host_bridge_smb_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + InputMask::WebDAV => { + let view_ids = self.get_host_bridge_webdav_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + } + } + + fn render_remote_input_mask( + &mut self, + f: &mut tuirealm::tui::Frame<'_>, + area: tuirealm::tui::layout::Rect, + ) { + let protocol_and_mask_chunks = Layout::default() + .constraints( + [ + Constraint::Length(3), // protocol + Constraint::Length(12), // Input mask + ] + .as_ref(), + ) + .direction(Direction::Vertical) + .split(area); + + self.app.view( + &Id::Remote(AuthFormId::Protocol), + f, + protocol_and_mask_chunks[0], + ); + + let input_mask = Layout::default() + .constraints( + [ + Constraint::Length(3), // uri + Constraint::Length(3), // username + Constraint::Length(3), // password + Constraint::Length(3), // dir + ] + .as_ref(), + ) + .direction(Direction::Vertical) + .split(protocol_and_mask_chunks[1]); + // Render input mask + match self.remote_input_mask() { + InputMask::AwsS3 => { + let view_ids = self.get_remote_s3_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + InputMask::Generic => { + let view_ids = self.get_remote_generic_params_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + InputMask::Kube => { + let view_ids = self.get_remote_kube_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + InputMask::Localhost => unreachable!(), + InputMask::Smb => { + let view_ids = self.get_remote_smb_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + InputMask::WebDAV => { + let view_ids = self.get_remote_webdav_view(); + self.app.view(&view_ids[0], f, input_mask[0]); + self.app.view(&view_ids[1], f, input_mask[1]); + self.app.view(&view_ids[2], f, input_mask[2]); + self.app.view(&view_ids[3], f, input_mask[3]); + } + } + } + /// Make text span from bookmarks pub(super) fn view_bookmarks(&mut self) { let bookmarks: Vec = self @@ -466,14 +620,14 @@ impl AuthActivity { } /// Mount bookmark save dialog - pub(super) fn mount_bookmark_save_dialog(&mut self) { + pub(super) fn mount_bookmark_save_dialog(&mut self, form_tab: FormTab) { let save_color = self.theme().misc_save_dialog; let warn_color = self.theme().misc_warn_dialog; assert!(self .app .remount( Id::BookmarkName, - Box::new(components::BookmarkName::new(save_color)), + Box::new(components::BookmarkName::new(form_tab, save_color)), vec![] ) .is_ok()); @@ -481,7 +635,7 @@ impl AuthActivity { .app .remount( Id::BookmarkSavePassword, - Box::new(components::BookmarkSavePassword::new(warn_color)), + Box::new(components::BookmarkSavePassword::new(form_tab, warn_color)), vec![] ) .is_ok()); @@ -548,26 +702,14 @@ impl AuthActivity { let _ = self.app.umount(&Id::InstallUpdatePopup); } - pub(super) fn mount_protocol(&mut self, protocol: FileTransferProtocol) { + pub(super) fn mount_host_bridge_protocol(&mut self, protocol: HostBridgeProtocol) { let protocol_color = self.theme().auth_protocol; assert!(self .app .remount( - Id::Protocol, - Box::new(components::ProtocolRadio::new(protocol, protocol_color)), - vec![] - ) - .is_ok()); - } - - pub(super) fn mount_remote_directory>(&mut self, remote_path: S) { - let protocol_color = self.theme().auth_protocol; - assert!(self - .app - .remount( - Id::RemoteDirectory, - Box::new(components::InputRemoteDirectory::new( - remote_path.as_ref(), + Id::HostBridge(AuthFormId::Protocol), + Box::new(components::HostBridgeProtocolRadio::new( + protocol, protocol_color )), vec![] @@ -575,14 +717,56 @@ impl AuthActivity { .is_ok()); } - pub(super) fn mount_local_directory>(&mut self, local_path: S) { + pub(super) fn mount_remote_protocol(&mut self, protocol: FileTransferProtocol) { + let protocol_color = self.theme().auth_protocol; + assert!(self + .app + .remount( + Id::Remote(AuthFormId::Protocol), + Box::new(components::RemoteProtocolRadio::new( + protocol, + protocol_color + )), + vec![] + ) + .is_ok()); + } + + pub(super) fn mount_remote_directory>( + &mut self, + form_tab: FormTab, + remote_path: S, + ) { + let id = Self::form_tab_id(form_tab, AuthFormId::RemoteDirectory); + let protocol_color = self.theme().auth_protocol; + assert!(self + .app + .remount( + id, + Box::new(components::InputRemoteDirectory::new( + remote_path.as_ref(), + form_tab, + protocol_color + )), + vec![] + ) + .is_ok()); + } + + pub(super) fn mount_local_directory>( + &mut self, + form_tab: FormTab, + local_path: S, + ) { + let id = Self::form_tab_id(form_tab, AuthFormId::LocalDirectory); let color = self.theme().auth_username; assert!(self .app .remount( - Id::LocalDirectory, + id, Box::new(components::InputLocalDirectory::new( local_path.as_ref(), + form_tab, color )), vec![] @@ -590,267 +774,318 @@ impl AuthActivity { .is_ok()); } - pub(super) fn mount_address(&mut self, address: &str) { + pub(super) fn mount_address(&mut self, form_tab: FormTab, address: &str) { let addr_color = self.theme().auth_address; + let id = Self::form_tab_id(form_tab, AuthFormId::Address); assert!(self .app .remount( - Id::Address, - Box::new(components::InputAddress::new(address, addr_color)), + id, + Box::new(components::InputAddress::new(address, form_tab, addr_color)), vec![] ) .is_ok()); } - pub(super) fn mount_port(&mut self, port: u16) { + pub(super) fn mount_port(&mut self, form_tab: FormTab, port: u16) { let port_color = self.theme().auth_port; + let id = Self::form_tab_id(form_tab, AuthFormId::Port); assert!(self .app .remount( - Id::Port, - Box::new(components::InputPort::new(port, port_color)), + id, + Box::new(components::InputPort::new(port, form_tab, port_color)), vec![] ) .is_ok()); } - pub(crate) fn mount_username(&mut self, username: &str) { + pub(super) fn mount_username(&mut self, form_tab: FormTab, username: &str) { let username_color = self.theme().auth_username; + let id = Self::form_tab_id(form_tab, AuthFormId::Username); assert!(self .app .remount( - Id::Username, - Box::new(components::InputUsername::new(username, username_color)), + id, + Box::new(components::InputUsername::new( + username, + form_tab, + username_color + )), vec![] ) .is_ok()); } - pub(crate) fn mount_password(&mut self, password: &str) { + pub(super) fn mount_password(&mut self, form_tab: FormTab, password: &str) { let password_color = self.theme().auth_password; + let id = Self::form_tab_id(form_tab, AuthFormId::Password); assert!(self .app .remount( - Id::Password, - Box::new(components::InputPassword::new(password, password_color)), + id, + Box::new(components::InputPassword::new( + password, + form_tab, + password_color + )), vec![] ) .is_ok()); } - pub(super) fn mount_s3_bucket(&mut self, bucket: &str) { + pub(super) fn mount_s3_bucket(&mut self, form_tab: FormTab, bucket: &str) { let addr_color = self.theme().auth_address; + let id = Self::form_tab_id(form_tab, AuthFormId::S3Bucket); assert!(self .app .remount( - Id::S3Bucket, - Box::new(components::InputS3Bucket::new(bucket, addr_color)), + id, + Box::new(components::InputS3Bucket::new(bucket, form_tab, addr_color)), vec![] ) .is_ok()); } - pub(super) fn mount_s3_region(&mut self, region: &str) { + pub(super) fn mount_s3_region(&mut self, form_tab: FormTab, region: &str) { let port_color = self.theme().auth_port; + let id = Self::form_tab_id(form_tab, AuthFormId::S3Region); assert!(self .app .remount( - Id::S3Region, - Box::new(components::InputS3Region::new(region, port_color)), + id, + Box::new(components::InputS3Region::new(region, form_tab, port_color)), vec![] ) .is_ok()); } - pub(crate) fn mount_s3_endpoint(&mut self, endpoint: &str) { + pub(super) fn mount_s3_endpoint(&mut self, form_tab: FormTab, endpoint: &str) { let username_color = self.theme().auth_username; + let id = Self::form_tab_id(form_tab, AuthFormId::S3Endpoint); assert!(self .app .remount( - Id::S3Endpoint, - Box::new(components::InputS3Endpoint::new(endpoint, username_color)), + id, + Box::new(components::InputS3Endpoint::new( + endpoint, + form_tab, + username_color + )), vec![] ) .is_ok()); } - pub(crate) fn mount_s3_profile(&mut self, profile: &str) { + pub(super) fn mount_s3_profile(&mut self, form_tab: FormTab, profile: &str) { let color = self.theme().auth_password; + let id = Self::form_tab_id(form_tab, AuthFormId::S3Profile); assert!(self .app .remount( - Id::S3Profile, - Box::new(components::InputS3Profile::new(profile, color)), + id, + Box::new(components::InputS3Profile::new(profile, form_tab, color)), vec![] ) .is_ok()); } - pub(crate) fn mount_s3_access_key(&mut self, key: &str) { + pub(super) fn mount_s3_access_key(&mut self, form_tab: FormTab, key: &str) { let color = self.theme().auth_address; + let id = Self::form_tab_id(form_tab, AuthFormId::S3AccessKey); assert!(self .app .remount( - Id::S3AccessKey, - Box::new(components::InputS3AccessKey::new(key, color)), + id, + Box::new(components::InputS3AccessKey::new(key, form_tab, color)), vec![] ) .is_ok()); } - pub(crate) fn mount_s3_secret_access_key(&mut self, key: &str) { + pub(super) fn mount_s3_secret_access_key(&mut self, form_tab: FormTab, key: &str) { let color = self.theme().auth_port; + let id = Self::form_tab_id(form_tab, AuthFormId::S3SecretAccessKey); assert!(self .app .remount( - Id::S3SecretAccessKey, - Box::new(components::InputS3SecretAccessKey::new(key, color)), + id, + Box::new(components::InputS3SecretAccessKey::new( + key, form_tab, color + )), vec![] ) .is_ok()); } - pub(crate) fn mount_s3_security_token(&mut self, token: &str) { + pub(super) fn mount_s3_security_token(&mut self, form_tab: FormTab, token: &str) { let color = self.theme().auth_username; + let id = Self::form_tab_id(form_tab, AuthFormId::S3SecurityToken); assert!(self .app .remount( - Id::S3SecurityToken, - Box::new(components::InputS3SecurityToken::new(token, color)), + id, + Box::new(components::InputS3SecurityToken::new( + token, form_tab, color + )), vec![] ) .is_ok()); } - pub(crate) fn mount_s3_session_token(&mut self, token: &str) { + pub(super) fn mount_s3_session_token(&mut self, form_tab: FormTab, token: &str) { let color = self.theme().auth_password; + let id = Self::form_tab_id(form_tab, AuthFormId::S3SessionToken); assert!(self .app .remount( - Id::S3SessionToken, - Box::new(components::InputS3SessionToken::new(token, color)), + id, + Box::new(components::InputS3SessionToken::new(token, form_tab, color)), vec![] ) .is_ok()); } - pub(crate) fn mount_s3_new_path_style(&mut self, new_path_style: bool) { + pub(super) fn mount_s3_new_path_style(&mut self, form_tab: FormTab, new_path_style: bool) { let color = self.theme().auth_address; + let id = Self::form_tab_id(form_tab, AuthFormId::S3NewPathStyle); assert!(self .app .remount( - Id::S3NewPathStyle, - Box::new(components::RadioS3NewPathStyle::new(new_path_style, color)), + id, + Box::new(components::RadioS3NewPathStyle::new( + new_path_style, + form_tab, + color + )), vec![] ) .is_ok()); } - pub(super) fn mount_kube_namespace(&mut self, value: &str) { + pub(super) fn mount_kube_namespace(&mut self, form_tab: FormTab, value: &str) { let color = self.theme().auth_port; + let id = Self::form_tab_id(form_tab, AuthFormId::KubeNamespace); assert!(self .app .remount( - Id::KubeNamespace, - Box::new(components::InputKubeNamespace::new(value, color)), + id, + Box::new(components::InputKubeNamespace::new(value, form_tab, color)), vec![] ) .is_ok()); } - pub(super) fn mount_kube_cluster_url(&mut self, value: &str) { + pub(super) fn mount_kube_cluster_url(&mut self, form_tab: FormTab, value: &str) { let color = self.theme().auth_username; + let id = Self::form_tab_id(form_tab, AuthFormId::KubeClusterUrl); assert!(self .app .remount( - Id::KubeClusterUrl, - Box::new(components::InputKubeClusterUrl::new(value, color)), + id, + Box::new(components::InputKubeClusterUrl::new(value, form_tab, color)), vec![] ) .is_ok()); } - pub(super) fn mount_kube_username(&mut self, value: &str) { + pub(super) fn mount_kube_username(&mut self, form_tab: FormTab, value: &str) { let color = self.theme().auth_password; + let id = Self::form_tab_id(form_tab, AuthFormId::KubeUsername); assert!(self .app .remount( - Id::KubeUsername, - Box::new(components::InputKubeUsername::new(value, color)), + id, + Box::new(components::InputKubeUsername::new(value, form_tab, color)), vec![] ) .is_ok()); } - pub(super) fn mount_kube_client_cert(&mut self, value: &str) { + pub(super) fn mount_kube_client_cert(&mut self, form_tab: FormTab, value: &str) { let color = self.theme().auth_address; + let id = Self::form_tab_id(form_tab, AuthFormId::KubeClientCert); assert!(self .app .remount( - Id::KubeClientCert, - Box::new(components::InputKubeClientCert::new(value, color)), + id, + Box::new(components::InputKubeClientCert::new(value, form_tab, color)), vec![] ) .is_ok()); } - pub(super) fn mount_kube_client_key(&mut self, value: &str) { + pub(super) fn mount_kube_client_key(&mut self, form_tab: FormTab, value: &str) { let color = self.theme().auth_port; + let id = Self::form_tab_id(form_tab, AuthFormId::KubeClientKey); assert!(self .app .remount( - Id::KubeClientKey, - Box::new(components::InputKubeClientKey::new(value, color)), + id, + Box::new(components::InputKubeClientKey::new(value, form_tab, color)), vec![] ) .is_ok()); } - pub(crate) fn mount_smb_share(&mut self, share: &str) { + pub(super) fn mount_smb_share(&mut self, form_tab: FormTab, share: &str) { let color = self.theme().auth_password; + let id = Self::form_tab_id(form_tab, AuthFormId::SmbShare); + assert!(self .app .remount( - Id::SmbShare, - Box::new(components::InputSmbShare::new(share, color)), + id, + Box::new(components::InputSmbShare::new(share, form_tab, color)), vec![] ) .is_ok()); } #[cfg(unix)] - pub(crate) fn mount_smb_workgroup(&mut self, workgroup: &str) { + pub(super) fn mount_smb_workgroup(&mut self, form_tab: FormTab, workgroup: &str) { let color = self.theme().auth_address; + let id = Self::form_tab_id(form_tab, AuthFormId::SmbWorkgroup); assert!(self .app .remount( - Id::SmbWorkgroup, - Box::new(components::InputSmbWorkgroup::new(workgroup, color)), + id, + Box::new(components::InputSmbWorkgroup::new( + workgroup, form_tab, color + )), vec![] ) .is_ok()); } - pub(super) fn mount_webdav_uri(&mut self, uri: &str) { + pub(super) fn mount_webdav_uri(&mut self, form_tab: FormTab, uri: &str) { let addr_color = self.theme().auth_address; + let id = Self::form_tab_id(form_tab, AuthFormId::WebDAVUri); assert!(self .app .remount( - Id::WebDAVUri, - Box::new(components::InputWebDAVUri::new(uri, addr_color)), + id, + Box::new(components::InputWebDAVUri::new(uri, form_tab, addr_color)), vec![] ) .is_ok()); } + fn form_tab_id(form_tab: FormTab, id: AuthFormId) -> Id { + match form_tab { + FormTab::HostBridge => Id::HostBridge(id), + FormTab::Remote => Id::Remote(id), + } + } + // -- query /// Collect input values from view - pub(super) fn get_generic_params_input(&self) -> GenericProtocolParams { - let addr: String = self.get_input_addr(); - let port: u16 = self.get_input_port(); - let username = self.get_input_username(); - let password = self.get_input_password(); + pub(super) fn get_generic_params_input(&self, form_tab: FormTab) -> GenericProtocolParams { + let addr: String = self.get_input_addr(form_tab); + let port: u16 = self.get_input_port(form_tab); + let username = self.get_input_username(form_tab); + let password = self.get_input_password(form_tab); GenericProtocolParams::default() .address(addr) .port(port) @@ -859,16 +1094,16 @@ impl AuthActivity { } /// Collect s3 input values from view - pub(super) fn get_s3_params_input(&self) -> AwsS3Params { - let bucket: String = self.get_input_s3_bucket(); - let region: Option = self.get_input_s3_region(); - let endpoint = self.get_input_s3_endpoint(); - let profile: Option = self.get_input_s3_profile(); - let access_key = self.get_input_s3_access_key(); - let secret_access_key = self.get_input_s3_secret_access_key(); - let security_token = self.get_input_s3_security_token(); - let session_token = self.get_input_s3_session_token(); - let new_path_style = self.get_input_s3_new_path_style(); + pub(super) fn get_s3_params_input(&self, form_tab: FormTab) -> AwsS3Params { + let bucket: String = self.get_input_s3_bucket(form_tab); + let region: Option = self.get_input_s3_region(form_tab); + let endpoint = self.get_input_s3_endpoint(form_tab); + let profile: Option = self.get_input_s3_profile(form_tab); + let access_key = self.get_input_s3_access_key(form_tab); + let secret_access_key = self.get_input_s3_secret_access_key(form_tab); + let security_token = self.get_input_s3_security_token(form_tab); + let session_token = self.get_input_s3_session_token(form_tab); + let new_path_style = self.get_input_s3_new_path_style(form_tab); AwsS3Params::new(bucket, region, profile) .endpoint(endpoint) .access_key(access_key) @@ -879,12 +1114,12 @@ impl AuthActivity { } /// Collect s3 input values from view - pub(super) fn get_kube_params_input(&self) -> KubeProtocolParams { - let namespace = self.get_input_kube_namespace(); - let cluster_url = self.get_input_kube_cluster_url(); - let username = self.get_input_kube_username(); - let client_cert = self.get_input_kube_client_cert(); - let client_key = self.get_input_kube_client_key(); + pub(super) fn get_kube_params_input(&self, form_tab: FormTab) -> KubeProtocolParams { + let namespace = self.get_input_kube_namespace(form_tab); + let cluster_url = self.get_input_kube_cluster_url(form_tab); + let username = self.get_input_kube_username(form_tab); + let client_cert = self.get_input_kube_client_cert(form_tab); + let client_key = self.get_input_kube_client_key(form_tab); KubeProtocolParams { namespace, cluster_url, @@ -896,14 +1131,14 @@ impl AuthActivity { /// Collect s3 input values from view #[cfg(unix)] - pub(super) fn get_smb_params_input(&self) -> SmbParams { - let share: String = self.get_input_smb_share(); - let workgroup: Option = self.get_input_smb_workgroup(); + pub(super) fn get_smb_params_input(&self, form_tab: FormTab) -> SmbParams { + let share: String = self.get_input_smb_share(form_tab); + let workgroup: Option = self.get_input_smb_workgroup(form_tab); - let address: String = self.get_input_addr(); - let port: u16 = self.get_input_port(); - let username = self.get_input_username(); - let password = self.get_input_password(); + let address: String = self.get_input_addr(form_tab); + let port: u16 = self.get_input_port(form_tab); + let username = self.get_input_username(form_tab); + let password = self.get_input_password(form_tab); SmbParams::new(address, share) .port(port) @@ -913,22 +1148,22 @@ impl AuthActivity { } #[cfg(windows)] - pub(super) fn get_smb_params_input(&self) -> SmbParams { - let share: String = self.get_input_smb_share(); + pub(super) fn get_smb_params_input(&self, form_tab: FormTab) -> SmbParams { + let share: String = self.get_input_smb_share(form_tab); - let address: String = self.get_input_addr(); - let username = self.get_input_username(); - let password = self.get_input_password(); + let address: String = self.get_input_addr(form_tab); + let username = self.get_input_username(form_tab); + let password = self.get_input_password(form_tab); SmbParams::new(address, share) .username(username) .password(password) } - pub(super) fn get_webdav_params_input(&self) -> WebDAVProtocolParams { - let uri: String = self.get_webdav_uri(); - let username = self.get_input_username().unwrap_or_default(); - let password = self.get_input_password().unwrap_or_default(); + pub(super) fn get_webdav_params_input(&self, form_tab: FormTab) -> WebDAVProtocolParams { + let uri: String = self.get_webdav_uri(form_tab); + let username = self.get_input_username(form_tab).unwrap_or_default(); + let password = self.get_input_password(form_tab).unwrap_or_default(); WebDAVProtocolParams { uri, @@ -937,8 +1172,11 @@ impl AuthActivity { } } - pub(super) fn get_input_remote_directory(&self) -> Option { - match self.app.state(&Id::RemoteDirectory) { + pub(super) fn get_input_remote_directory(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::RemoteDirectory)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => { Some(PathBuf::from(x.as_str())) } @@ -946,8 +1184,11 @@ impl AuthActivity { } } - pub(super) fn get_input_local_directory(&self) -> Option { - match self.app.state(&Id::LocalDirectory) { + pub(super) fn get_input_local_directory(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::LocalDirectory)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => { Some(PathBuf::from(x.as_str())) } @@ -955,149 +1196,210 @@ impl AuthActivity { } } - pub(super) fn get_webdav_uri(&self) -> String { - match self.app.state(&Id::WebDAVUri) { + pub(super) fn get_webdav_uri(&self, form_tab: FormTab) -> String { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::WebDAVUri)) + { Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } - pub(super) fn get_input_addr(&self) -> String { - match self.app.state(&Id::Address) { + pub(super) fn get_input_addr(&self, form_tab: FormTab) -> String { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::Address)) + { Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } - pub(super) fn get_input_port(&self) -> u16 { - match self.app.state(&Id::Port) { + pub(super) fn get_input_port(&self, form_tab: FormTab) -> u16 { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::Port)) + { Ok(State::One(StateValue::String(x))) => u16::from_str(x.as_str()).unwrap_or_default(), _ => 0, } } - pub(super) fn get_input_username(&self) -> Option { - match self.app.state(&Id::Username) { + pub(super) fn get_input_username(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::Username)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_password(&self) -> Option { - match self.app.state(&Id::Password) { + pub(super) fn get_input_password(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::Password)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_s3_bucket(&self) -> String { - match self.app.state(&Id::S3Bucket) { + pub(super) fn get_input_s3_bucket(&self, form_tab: FormTab) -> String { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3Bucket)) + { Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } - pub(super) fn get_input_s3_region(&self) -> Option { - match self.app.state(&Id::S3Region) { + pub(super) fn get_input_s3_region(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3Region)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_s3_endpoint(&self) -> Option { - match self.app.state(&Id::S3Endpoint) { + pub(super) fn get_input_s3_endpoint(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3Endpoint)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_s3_profile(&self) -> Option { - match self.app.state(&Id::S3Profile) { + pub(super) fn get_input_s3_profile(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3Profile)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_s3_access_key(&self) -> Option { - match self.app.state(&Id::S3AccessKey) { + pub(super) fn get_input_s3_access_key(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3AccessKey)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_s3_secret_access_key(&self) -> Option { - match self.app.state(&Id::S3SecretAccessKey) { + pub(super) fn get_input_s3_secret_access_key(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3SecretAccessKey)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_s3_security_token(&self) -> Option { - match self.app.state(&Id::S3SecurityToken) { + pub(super) fn get_input_s3_security_token(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3SecurityToken)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_s3_session_token(&self) -> Option { - match self.app.state(&Id::S3SessionToken) { + pub(super) fn get_input_s3_session_token(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3SessionToken)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_s3_new_path_style(&self) -> bool { + pub(super) fn get_input_s3_new_path_style(&self, form_tab: FormTab) -> bool { matches!( - self.app.state(&Id::S3NewPathStyle), + self.app + .state(&Self::form_tab_id(form_tab, AuthFormId::S3NewPathStyle)), Ok(State::One(StateValue::Usize(0))) ) } - pub(super) fn get_input_kube_namespace(&self) -> Option { - match self.app.state(&Id::KubeNamespace) { + pub(super) fn get_input_kube_namespace(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::KubeNamespace)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_kube_cluster_url(&self) -> Option { - match self.app.state(&Id::KubeClusterUrl) { + pub(super) fn get_input_kube_cluster_url(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::KubeClusterUrl)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_kube_username(&self) -> Option { - match self.app.state(&Id::KubeUsername) { + pub(super) fn get_input_kube_username(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::KubeUsername)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_kube_client_cert(&self) -> Option { - match self.app.state(&Id::KubeClientCert) { + pub(super) fn get_input_kube_client_cert(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::KubeClientCert)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_kube_client_key(&self) -> Option { - match self.app.state(&Id::KubeClientKey) { + pub(super) fn get_input_kube_client_key(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::KubeClientKey)) + { Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), _ => None, } } - pub(super) fn get_input_smb_share(&self) -> String { - match self.app.state(&Id::SmbShare) { + pub(super) fn get_input_smb_share(&self, form_tab: FormTab) -> String { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::SmbShare)) + { Ok(State::One(StateValue::String(x))) => x, _ => String::new(), } } #[cfg(unix)] - pub(super) fn get_input_smb_workgroup(&self) -> Option { - match self.app.state(&Id::SmbWorkgroup) { + pub(super) fn get_input_smb_workgroup(&self, form_tab: FormTab) -> Option { + match self + .app + .state(&Self::form_tab_id(form_tab, AuthFormId::SmbWorkgroup)) + { Ok(State::One(StateValue::String(x))) => Some(x), _ => None, } @@ -1121,12 +1423,20 @@ impl AuthActivity { // -- len - /// Returns the input mask size based on current input mask - pub(super) fn input_mask_size(&self) -> u16 { - match self.input_mask() { + /// Returns the max input mask size based on current input mask + fn max_input_mask_size(&self) -> u16 { + Self::input_mask_size(self.host_bridge_input_mask()) + .max(Self::input_mask_size(self.remote_input_mask())) + + 3 // +3 because of protocol + } + + /// Get the input mask size based on input mask + fn input_mask_size(input_mask: InputMask) -> u16 { + match input_mask { InputMask::AwsS3 => 12, InputMask::Generic => 12, InputMask::Kube => 12, + InputMask::Localhost => 3, InputMask::Smb => 12, InputMask::WebDAV => 12, } @@ -1208,162 +1518,431 @@ impl AuthActivity { } /// Get the visible element in the generic params form, based on current focus - fn get_generic_params_view(&self) -> [Id; 4] { + fn get_host_bridge_generic_params_view(&self) -> [Id; 4] { match self.app.focus() { - Some(&Id::RemoteDirectory) => { - [Id::Port, Id::Username, Id::Password, Id::RemoteDirectory] - } - Some(&Id::LocalDirectory) => [ - Id::Username, - Id::Password, - Id::RemoteDirectory, - Id::LocalDirectory, + Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [ + Id::HostBridge(AuthFormId::Port), + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::RemoteDirectory), + ], + Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [ + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::RemoteDirectory), + Id::HostBridge(AuthFormId::LocalDirectory), + ], + _ => [ + Id::HostBridge(AuthFormId::Address), + Id::HostBridge(AuthFormId::Port), + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + ], + } + } + + /// Get the visible element in the generic params form, based on current focus + fn get_remote_generic_params_view(&self) -> [Id; 4] { + match self.app.focus() { + Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [ + Id::Remote(AuthFormId::Port), + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::RemoteDirectory), + ], + Some(&Id::Remote(AuthFormId::LocalDirectory)) => [ + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::RemoteDirectory), + Id::Remote(AuthFormId::LocalDirectory), + ], + _ => [ + Id::Remote(AuthFormId::Address), + Id::Remote(AuthFormId::Port), + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + ], + } + } + + fn get_host_bridge_localhost_view(&self) -> [Id; 1] { + [Id::HostBridge(AuthFormId::LocalDirectory)] + } + + /// Get the visible element in the aws-s3 form, based on current focus + fn get_host_bridge_s3_view(&self) -> [Id; 4] { + match self.app.focus() { + Some(&Id::HostBridge(AuthFormId::S3AccessKey)) => [ + Id::HostBridge(AuthFormId::S3Region), + Id::HostBridge(AuthFormId::S3Endpoint), + Id::HostBridge(AuthFormId::S3Profile), + Id::HostBridge(AuthFormId::S3AccessKey), + ], + Some(&Id::HostBridge(AuthFormId::S3SecretAccessKey)) => [ + Id::HostBridge(AuthFormId::S3Endpoint), + Id::HostBridge(AuthFormId::S3Profile), + Id::HostBridge(AuthFormId::S3AccessKey), + Id::HostBridge(AuthFormId::S3SecretAccessKey), + ], + Some(&Id::HostBridge(AuthFormId::S3SecurityToken)) => [ + Id::HostBridge(AuthFormId::S3Profile), + Id::HostBridge(AuthFormId::S3AccessKey), + Id::HostBridge(AuthFormId::S3SecretAccessKey), + Id::HostBridge(AuthFormId::S3SecurityToken), + ], + Some(&Id::HostBridge(AuthFormId::S3SessionToken)) => [ + Id::HostBridge(AuthFormId::S3AccessKey), + Id::HostBridge(AuthFormId::S3SecretAccessKey), + Id::HostBridge(AuthFormId::S3SecurityToken), + Id::HostBridge(AuthFormId::S3SessionToken), + ], + Some(&Id::HostBridge(AuthFormId::S3NewPathStyle)) => [ + Id::HostBridge(AuthFormId::S3SecretAccessKey), + Id::HostBridge(AuthFormId::S3SecurityToken), + Id::HostBridge(AuthFormId::S3SessionToken), + Id::HostBridge(AuthFormId::S3NewPathStyle), + ], + Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [ + Id::HostBridge(AuthFormId::S3SecurityToken), + Id::HostBridge(AuthFormId::S3SessionToken), + Id::HostBridge(AuthFormId::S3NewPathStyle), + Id::HostBridge(AuthFormId::RemoteDirectory), + ], + Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [ + Id::HostBridge(AuthFormId::S3SessionToken), + Id::HostBridge(AuthFormId::S3NewPathStyle), + Id::HostBridge(AuthFormId::RemoteDirectory), + Id::HostBridge(AuthFormId::LocalDirectory), + ], + _ => [ + Id::HostBridge(AuthFormId::S3Bucket), + Id::HostBridge(AuthFormId::S3Region), + Id::HostBridge(AuthFormId::S3Endpoint), + Id::HostBridge(AuthFormId::S3Profile), ], - _ => [Id::Address, Id::Port, Id::Username, Id::Password], } } /// Get the visible element in the aws-s3 form, based on current focus - fn get_s3_view(&self) -> [Id; 4] { + fn get_remote_s3_view(&self) -> [Id; 4] { match self.app.focus() { - Some(&Id::S3AccessKey) => { - [Id::S3Region, Id::S3Endpoint, Id::S3Profile, Id::S3AccessKey] - } - Some(&Id::S3SecretAccessKey) => [ - Id::S3Endpoint, - Id::S3Profile, - Id::S3AccessKey, - Id::S3SecretAccessKey, + Some(&Id::Remote(AuthFormId::S3AccessKey)) => [ + Id::Remote(AuthFormId::S3Region), + Id::Remote(AuthFormId::S3Endpoint), + Id::Remote(AuthFormId::S3Profile), + Id::Remote(AuthFormId::S3AccessKey), ], - Some(&Id::S3SecurityToken) => [ - Id::S3Profile, - Id::S3AccessKey, - Id::S3SecretAccessKey, - Id::S3SecurityToken, + Some(&Id::Remote(AuthFormId::S3SecretAccessKey)) => [ + Id::Remote(AuthFormId::S3Endpoint), + Id::Remote(AuthFormId::S3Profile), + Id::Remote(AuthFormId::S3AccessKey), + Id::Remote(AuthFormId::S3SecretAccessKey), ], - Some(&Id::S3SessionToken) => [ - Id::S3AccessKey, - Id::S3SecretAccessKey, - Id::S3SecurityToken, - Id::S3SessionToken, + Some(&Id::Remote(AuthFormId::S3SecurityToken)) => [ + Id::Remote(AuthFormId::S3Profile), + Id::Remote(AuthFormId::S3AccessKey), + Id::Remote(AuthFormId::S3SecretAccessKey), + Id::Remote(AuthFormId::S3SecurityToken), ], - Some(&Id::S3NewPathStyle) => [ - Id::S3SecretAccessKey, - Id::S3SecurityToken, - Id::S3SessionToken, - Id::S3NewPathStyle, + Some(&Id::Remote(AuthFormId::S3SessionToken)) => [ + Id::Remote(AuthFormId::S3AccessKey), + Id::Remote(AuthFormId::S3SecretAccessKey), + Id::Remote(AuthFormId::S3SecurityToken), + Id::Remote(AuthFormId::S3SessionToken), ], - Some(&Id::RemoteDirectory) => [ - Id::S3SecurityToken, - Id::S3SessionToken, - Id::S3NewPathStyle, - Id::RemoteDirectory, + Some(&Id::Remote(AuthFormId::S3NewPathStyle)) => [ + Id::Remote(AuthFormId::S3SecretAccessKey), + Id::Remote(AuthFormId::S3SecurityToken), + Id::Remote(AuthFormId::S3SessionToken), + Id::Remote(AuthFormId::S3NewPathStyle), ], - Some(&Id::LocalDirectory) => [ - Id::S3SessionToken, - Id::S3NewPathStyle, - Id::RemoteDirectory, - Id::LocalDirectory, + Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [ + Id::Remote(AuthFormId::S3SecurityToken), + Id::Remote(AuthFormId::S3SessionToken), + Id::Remote(AuthFormId::S3NewPathStyle), + Id::Remote(AuthFormId::RemoteDirectory), + ], + Some(&Id::Remote(AuthFormId::LocalDirectory)) => [ + Id::Remote(AuthFormId::S3SessionToken), + Id::Remote(AuthFormId::S3NewPathStyle), + Id::Remote(AuthFormId::RemoteDirectory), + Id::Remote(AuthFormId::LocalDirectory), + ], + _ => [ + Id::Remote(AuthFormId::S3Bucket), + Id::Remote(AuthFormId::S3Region), + Id::Remote(AuthFormId::S3Endpoint), + Id::Remote(AuthFormId::S3Profile), ], - _ => [Id::S3Bucket, Id::S3Region, Id::S3Endpoint, Id::S3Profile], } } /// Get the visible element in the kube form, based on current focus - fn get_kube_view(&self) -> [Id; 4] { + fn get_host_bridge_kube_view(&self) -> [Id; 4] { match self.app.focus() { - Some(&Id::KubeClientCert) => [ - Id::KubeNamespace, - Id::KubeClusterUrl, - Id::KubeUsername, - Id::KubeClientCert, + Some(&Id::HostBridge(AuthFormId::KubeClientCert)) => [ + Id::HostBridge(AuthFormId::KubeNamespace), + Id::HostBridge(AuthFormId::KubeClusterUrl), + Id::HostBridge(AuthFormId::KubeUsername), + Id::HostBridge(AuthFormId::KubeClientCert), ], - Some(&Id::KubeClientKey) => [ - Id::KubeClusterUrl, - Id::KubeUsername, - Id::KubeClientCert, - Id::KubeClientKey, + Some(&Id::HostBridge(AuthFormId::KubeClientKey)) => [ + Id::HostBridge(AuthFormId::KubeClusterUrl), + Id::HostBridge(AuthFormId::KubeUsername), + Id::HostBridge(AuthFormId::KubeClientCert), + Id::HostBridge(AuthFormId::KubeClientKey), ], - Some(&Id::RemoteDirectory) => [ - Id::KubeUsername, - Id::KubeClientCert, - Id::KubeClientKey, - Id::RemoteDirectory, + Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [ + Id::HostBridge(AuthFormId::KubeUsername), + Id::HostBridge(AuthFormId::KubeClientCert), + Id::HostBridge(AuthFormId::KubeClientKey), + Id::HostBridge(AuthFormId::RemoteDirectory), ], - Some(&Id::LocalDirectory) => [ - Id::KubeClientCert, - Id::KubeClientKey, - Id::RemoteDirectory, - Id::LocalDirectory, + Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [ + Id::HostBridge(AuthFormId::KubeClientCert), + Id::HostBridge(AuthFormId::KubeClientKey), + Id::HostBridge(AuthFormId::RemoteDirectory), + Id::HostBridge(AuthFormId::LocalDirectory), ], _ => [ - Id::KubeNamespace, - Id::KubeClusterUrl, - Id::KubeUsername, - Id::KubeClientCert, + Id::HostBridge(AuthFormId::KubeNamespace), + Id::HostBridge(AuthFormId::KubeClusterUrl), + Id::HostBridge(AuthFormId::KubeUsername), + Id::HostBridge(AuthFormId::KubeClientCert), + ], + } + } + + /// Get the visible element in the kube form, based on current focus + fn get_remote_kube_view(&self) -> [Id; 4] { + match self.app.focus() { + Some(&Id::Remote(AuthFormId::KubeClientCert)) => [ + Id::Remote(AuthFormId::KubeNamespace), + Id::Remote(AuthFormId::KubeClusterUrl), + Id::Remote(AuthFormId::KubeUsername), + Id::Remote(AuthFormId::KubeClientCert), + ], + Some(&Id::Remote(AuthFormId::KubeClientKey)) => [ + Id::Remote(AuthFormId::KubeClusterUrl), + Id::Remote(AuthFormId::KubeUsername), + Id::Remote(AuthFormId::KubeClientCert), + Id::Remote(AuthFormId::KubeClientKey), + ], + Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [ + Id::Remote(AuthFormId::KubeUsername), + Id::Remote(AuthFormId::KubeClientCert), + Id::Remote(AuthFormId::KubeClientKey), + Id::Remote(AuthFormId::RemoteDirectory), + ], + Some(&Id::Remote(AuthFormId::LocalDirectory)) => [ + Id::Remote(AuthFormId::KubeClientCert), + Id::Remote(AuthFormId::KubeClientKey), + Id::Remote(AuthFormId::RemoteDirectory), + Id::Remote(AuthFormId::LocalDirectory), + ], + _ => [ + Id::Remote(AuthFormId::KubeNamespace), + Id::Remote(AuthFormId::KubeClusterUrl), + Id::Remote(AuthFormId::KubeUsername), + Id::Remote(AuthFormId::KubeClientCert), ], } } #[cfg(unix)] - fn get_smb_view(&self) -> [Id; 4] { + fn get_host_bridge_smb_view(&self) -> [Id; 4] { match self.app.focus() { - Some(&Id::Address | &Id::Port | &Id::SmbShare | &Id::Username) => { - [Id::Address, Id::Port, Id::SmbShare, Id::Username] - } - Some(&Id::Password) => [Id::Port, Id::SmbShare, Id::Username, Id::Password], - Some(&Id::SmbWorkgroup) => [Id::SmbShare, Id::Username, Id::Password, Id::SmbWorkgroup], - Some(&Id::RemoteDirectory) => [ - Id::Username, - Id::Password, - Id::SmbWorkgroup, - Id::RemoteDirectory, + Some( + &Id::HostBridge(AuthFormId::Address) + | &Id::HostBridge(AuthFormId::Port) + | &Id::HostBridge(AuthFormId::SmbShare) + | &Id::HostBridge(AuthFormId::Username), + ) => [ + Id::HostBridge(AuthFormId::Address), + Id::HostBridge(AuthFormId::Port), + Id::HostBridge(AuthFormId::SmbShare), + Id::HostBridge(AuthFormId::Username), ], - Some(&Id::LocalDirectory) => [ - Id::Password, - Id::SmbWorkgroup, - Id::RemoteDirectory, - Id::LocalDirectory, + Some(&Id::HostBridge(AuthFormId::Password)) => [ + Id::HostBridge(AuthFormId::Port), + Id::HostBridge(AuthFormId::SmbShare), + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + ], + Some(&Id::HostBridge(AuthFormId::SmbWorkgroup)) => [ + Id::HostBridge(AuthFormId::SmbShare), + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::SmbWorkgroup), + ], + Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [ + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::SmbWorkgroup), + Id::HostBridge(AuthFormId::RemoteDirectory), + ], + Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [ + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::SmbWorkgroup), + Id::HostBridge(AuthFormId::RemoteDirectory), + Id::HostBridge(AuthFormId::LocalDirectory), + ], + _ => [ + Id::HostBridge(AuthFormId::Address), + Id::HostBridge(AuthFormId::Port), + Id::HostBridge(AuthFormId::SmbShare), + Id::HostBridge(AuthFormId::Username), + ], + } + } + + #[cfg(unix)] + fn get_remote_smb_view(&self) -> [Id; 4] { + match self.app.focus() { + Some( + &Id::Remote(AuthFormId::Address) + | &Id::Remote(AuthFormId::Port) + | &Id::Remote(AuthFormId::SmbShare) + | &Id::Remote(AuthFormId::Username), + ) => [ + Id::Remote(AuthFormId::Address), + Id::Remote(AuthFormId::Port), + Id::Remote(AuthFormId::SmbShare), + Id::Remote(AuthFormId::Username), + ], + Some(&Id::Remote(AuthFormId::Password)) => [ + Id::Remote(AuthFormId::Port), + Id::Remote(AuthFormId::SmbShare), + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + ], + Some(&Id::Remote(AuthFormId::SmbWorkgroup)) => [ + Id::Remote(AuthFormId::SmbShare), + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::SmbWorkgroup), + ], + Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [ + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::SmbWorkgroup), + Id::Remote(AuthFormId::RemoteDirectory), + ], + Some(&Id::Remote(AuthFormId::LocalDirectory)) => [ + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::SmbWorkgroup), + Id::Remote(AuthFormId::RemoteDirectory), + Id::Remote(AuthFormId::LocalDirectory), + ], + _ => [ + Id::Remote(AuthFormId::Address), + Id::Remote(AuthFormId::Port), + Id::Remote(AuthFormId::SmbShare), + Id::Remote(AuthFormId::Username), ], - _ => [Id::Address, Id::Port, Id::SmbShare, Id::Username], } } #[cfg(windows)] - fn get_smb_view(&self) -> [Id; 4] { + fn get_host_bridge_smb_view(&self) -> [Id; 4] { match self.app.focus() { - Some(&Id::Address | &Id::Password | &Id::SmbShare | &Id::Username) => { - [Id::Address, Id::SmbShare, Id::Username, Id::Password] - } - Some(&Id::RemoteDirectory) => [ - Id::SmbShare, - Id::Username, - Id::Password, - Id::RemoteDirectory, + Some( + &Id::HostBridge(AuthFormId::Address) + | &Id::HostBridge(AuthFormId::Password) + | &Id::HostBridge(AuthFormId::SmbShare) + | &Id::HostBridge(AuthFormId::Username), + ) => [ + Id::HostBridge(AuthFormId::Address), + Id::HostBridge(AuthFormId::SmbShare), + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), ], - Some(&Id::LocalDirectory) => [ - Id::Username, - Id::Password, - Id::RemoteDirectory, - Id::LocalDirectory, + Some(&Id::HostBridge(AuthFormId::RemoteDirectory)) => [ + Id::HostBridge(AuthFormId::SmbShare), + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::RemoteDirectory), + ], + Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [ + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::RemoteDirectory), + Id::HostBridge(AuthFormId::LocalDirectory), + ], + _ => [ + Id::HostBridge(AuthFormId::Address), + Id::HostBridge(AuthFormId::SmbShare), + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), ], - _ => [Id::Address, Id::SmbShare, Id::Username, Id::Password], } } - fn get_webdav_view(&self) -> [Id; 4] { + #[cfg(windows)] + fn get_remote_smb_view(&self) -> [Id; 4] { match self.app.focus() { - Some(&Id::LocalDirectory) => [ - Id::Username, - Id::Password, - Id::RemoteDirectory, - Id::LocalDirectory, + Some( + &Id::Remote(AuthFormId::Address) + | &Id::Remote(AuthFormId::Password) + | &Id::Remote(AuthFormId::SmbShare) + | &Id::Remote(AuthFormId::Username), + ) => [ + Id::Remote(AuthFormId::Address), + Id::Remote(AuthFormId::SmbShare), + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + ], + Some(&Id::Remote(AuthFormId::RemoteDirectory)) => [ + Id::Remote(AuthFormId::SmbShare), + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::RemoteDirectory), + ], + Some(&Id::Remote(AuthFormId::LocalDirectory)) => [ + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::RemoteDirectory), + Id::Remote(AuthFormId::LocalDirectory), ], _ => [ - Id::WebDAVUri, - Id::Username, - Id::Password, - Id::RemoteDirectory, + Id::Remote(AuthFormId::Address), + Id::Remote(AuthFormId::SmbShare), + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + ], + } + } + + fn get_host_bridge_webdav_view(&self) -> [Id; 4] { + match self.app.focus() { + Some(&Id::HostBridge(AuthFormId::LocalDirectory)) => [ + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::RemoteDirectory), + Id::HostBridge(AuthFormId::LocalDirectory), + ], + _ => [ + Id::HostBridge(AuthFormId::WebDAVUri), + Id::HostBridge(AuthFormId::Username), + Id::HostBridge(AuthFormId::Password), + Id::HostBridge(AuthFormId::RemoteDirectory), + ], + } + } + + fn get_remote_webdav_view(&self) -> [Id; 4] { + match self.app.focus() { + Some(&Id::Remote(AuthFormId::LocalDirectory)) => [ + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::RemoteDirectory), + Id::Remote(AuthFormId::LocalDirectory), + ], + _ => [ + Id::Remote(AuthFormId::WebDAVUri), + Id::Remote(AuthFormId::Username), + Id::Remote(AuthFormId::Password), + Id::Remote(AuthFormId::RemoteDirectory), ], } } @@ -1431,6 +2010,13 @@ impl AuthActivity { .is_ok()); } + pub(super) fn get_current_form_tab(&self) -> FormTab { + match self.app.focus() { + Some(&Id::HostBridge(_)) => FormTab::HostBridge, + _ => FormTab::Remote, + } + } + /// Returns a sub clause which requires that no popup is mounted in order to be satisfied fn no_popup_mounted_clause() -> SubClause { SubClause::And( diff --git a/src/ui/activities/filetransfer/actions/change_dir.rs b/src/ui/activities/filetransfer/actions/change_dir.rs index 975b0cf..db5c5a5 100644 --- a/src/ui/activities/filetransfer/actions/change_dir.rs +++ b/src/ui/activities/filetransfer/actions/change_dir.rs @@ -19,7 +19,7 @@ enum SyncBrowsingDestination { impl FileTransferActivity { /// Enter a directory on local host from entry pub(crate) fn action_enter_local_dir(&mut self, dir: File) { - self.local_changedir(dir.path(), true); + self.host_bridge_changedir(dir.path(), true); if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name())); } @@ -35,8 +35,9 @@ impl FileTransferActivity { /// Change local directory reading value from input pub(crate) fn action_change_local_dir(&mut self, input: String) { - let dir_path: PathBuf = self.local_to_abs_path(PathBuf::from(input.as_str()).as_path()); - self.local_changedir(dir_path.as_path(), true); + let dir_path: PathBuf = + self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path()); + self.host_bridge_changedir(dir_path.as_path(), true); // Check whether to sync if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::Path(input)); @@ -55,8 +56,8 @@ impl FileTransferActivity { /// Go to previous directory from localhost pub(crate) fn action_go_to_previous_local_dir(&mut self) { - if let Some(d) = self.local_mut().popd() { - self.local_changedir(d.as_path(), false); + if let Some(d) = self.host_bridge_mut().popd() { + self.host_bridge_changedir(d.as_path(), false); // Check whether to sync if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::PreviousDir); @@ -78,10 +79,10 @@ impl FileTransferActivity { /// Go to upper directory on local host pub(crate) fn action_go_to_local_upper_dir(&mut self) { // Get pwd - let path: PathBuf = self.local().wrkdir.clone(); + let path: PathBuf = self.host_bridge().wrkdir.clone(); // Go to parent directory if let Some(parent) = path.as_path().parent() { - self.local_changedir(parent, true); + self.host_bridge_changedir(parent, true); // If sync is enabled update remote too if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::ParentDir); @@ -118,7 +119,7 @@ impl FileTransferActivity { trace!("Synchronizing browsing to path {}", path.display()); // Check whether destination exists on host let exists = match self.browser.tab() { - FileExplorerTab::Local => match self.client.exists(path.as_path()) { + FileExplorerTab::HostBridge => match self.client.exists(path.as_path()) { Ok(e) => e, Err(err) => { error!( @@ -129,7 +130,17 @@ impl FileTransferActivity { return; } }, - FileExplorerTab::Remote => self.host.file_exists(path.as_path()), + FileExplorerTab::Remote => match self.host_bridge.exists(path.as_path()) { + Ok(e) => e, + Err(err) => { + error!( + "Failed to check whether {} exists on host: {}", + path.display(), + err + ); + return; + } + }, _ => return, }; let name = path @@ -150,7 +161,7 @@ impl FileTransferActivity { trace!("User wants to create the unexisting directory"); // Make directory match self.browser.tab() { - FileExplorerTab::Local => self.action_remote_mkdir(name.clone()), + FileExplorerTab::HostBridge => self.action_remote_mkdir(name.clone()), FileExplorerTab::Remote => self.action_local_mkdir(name.clone()), _ => {} } @@ -173,18 +184,18 @@ impl FileTransferActivity { // Enter directory match destination { SyncBrowsingDestination::ParentDir => match self.browser.tab() { - FileExplorerTab::Local => self.remote_changedir(path.as_path(), true), - FileExplorerTab::Remote => self.local_changedir(path.as_path(), true), + FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true), + FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true), _ => {} }, SyncBrowsingDestination::Path(_) => match self.browser.tab() { - FileExplorerTab::Local => self.remote_changedir(path.as_path(), true), - FileExplorerTab::Remote => self.local_changedir(path.as_path(), true), + FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true), + FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true), _ => {} }, SyncBrowsingDestination::PreviousDir => match self.browser.tab() { - FileExplorerTab::Local => self.remote_changedir(path.as_path(), false), - FileExplorerTab::Remote => self.local_changedir(path.as_path(), false), + FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false), + FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false), _ => {} }, } @@ -197,13 +208,13 @@ impl FileTransferActivity { ) -> Option { match (destination, self.browser.tab()) { // NOTE: tab and methods are switched on purpose - (SyncBrowsingDestination::ParentDir, FileExplorerTab::Local) => { + (SyncBrowsingDestination::ParentDir, FileExplorerTab::HostBridge) => { self.remote().wrkdir.parent().map(|x| x.to_path_buf()) } (SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => { - self.local().wrkdir.parent().map(|x| x.to_path_buf()) + self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf()) } - (SyncBrowsingDestination::PreviousDir, FileExplorerTab::Local) => { + (SyncBrowsingDestination::PreviousDir, FileExplorerTab::HostBridge) => { if let Some(p) = self.remote_mut().popd() { Some(p) } else { @@ -212,7 +223,7 @@ impl FileTransferActivity { } } (SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => { - if let Some(p) = self.local_mut().popd() { + if let Some(p) = self.host_bridge_mut().popd() { Some(p) } else { warn!("Cannot synchronize browsing: local has no previous directory in stack"); diff --git a/src/ui/activities/filetransfer/actions/chmod.rs b/src/ui/activities/filetransfer/actions/chmod.rs index bb0ac6c..61de0a7 100644 --- a/src/ui/activities/filetransfer/actions/chmod.rs +++ b/src/ui/activities/filetransfer/actions/chmod.rs @@ -3,12 +3,11 @@ use remotefs::fs::UnixPex; use super::{FileTransferActivity, LogLevel}; impl FileTransferActivity { - #[cfg(unix)] pub fn action_local_chmod(&mut self, mode: UnixPex) { let files = self.get_local_selected_entries().get_files(); for file in files { - if let Err(err) = self.host.chmod(file.path(), mode) { + if let Err(err) = self.host_bridge.chmod(file.path(), mode) { self.log_and_alert( LogLevel::Error, format!( @@ -51,12 +50,11 @@ impl FileTransferActivity { } } - #[cfg(unix)] pub fn action_find_local_chmod(&mut self, mode: UnixPex) { let files = self.get_found_selected_entries().get_files(); for file in files { - if let Err(err) = self.host.chmod(file.path(), mode) { + if let Err(err) = self.host_bridge.chmod(file.path(), mode) { self.log_and_alert( LogLevel::Error, format!( diff --git a/src/ui/activities/filetransfer/actions/copy.rs b/src/ui/activities/filetransfer/actions/copy.rs index dd020d9..735709a 100644 --- a/src/ui/activities/filetransfer/actions/copy.rs +++ b/src/ui/activities/filetransfer/actions/copy.rs @@ -53,7 +53,7 @@ impl FileTransferActivity { } fn local_copy_file(&mut self, entry: &File, dest: &Path) { - match self.host.copy(entry, dest) { + match self.host_bridge.copy(entry, dest) { Ok(_) => { self.log( LogLevel::Info, @@ -136,7 +136,7 @@ impl FileTransferActivity { return Err(err); } // Stat dir - let tempdir_entry = match self.host.stat(tempdir_path.as_path()) { + let tempdir_entry = match self.host_bridge.stat(tempdir_path.as_path()) { Ok(e) => e, Err(err) => { self.log_and_alert( @@ -189,7 +189,7 @@ impl FileTransferActivity { return Err(err); } // Get local fs entry - let tmpfile_entry = match self.host.stat(tmpfile.path()) { + let tmpfile_entry = match self.host_bridge.stat(tmpfile.path()) { Ok(e) if e.is_file() => e, Ok(_) => panic!("{} is not a file", tmpfile.path().display()), Err(err) => { diff --git a/src/ui/activities/filetransfer/actions/delete.rs b/src/ui/activities/filetransfer/actions/delete.rs index 65bd494..8e8d37d 100644 --- a/src/ui/activities/filetransfer/actions/delete.rs +++ b/src/ui/activities/filetransfer/actions/delete.rs @@ -43,7 +43,7 @@ impl FileTransferActivity { } pub(crate) fn local_remove_file(&mut self, entry: &File) { - match self.host.remove(entry) { + match self.host_bridge.remove(entry) { Ok(_) => { // Log self.log( diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index cd4302b..b93d909 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -2,13 +2,12 @@ //! //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall -// locals use std::fs::OpenOptions; use std::io::Read; use std::path::{Path, PathBuf}; use std::time::SystemTime; -// ext +use remotefs::fs::Metadata; use remotefs::File; use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; @@ -29,7 +28,12 @@ impl FileTransferActivity { format!("Opening file \"{}\"…", entry.path().display()), ); // Edit file - if let Err(err) = self.edit_local_file(entry.path()) { + let res = match self.host_bridge.is_localhost() { + true => self.edit_local_file(entry.path()).map(|_| ()), + false => self.edit_bridged_local_file(entry), + }; + + if let Err(err) = res { self.log_and_alert(LogLevel::Error, err); } } @@ -59,7 +63,83 @@ impl FileTransferActivity { } /// Edit a file on localhost - fn edit_local_file(&mut self, path: &Path) -> Result<(), String> { + fn edit_bridged_local_file(&mut self, entry: &File) -> Result<(), String> { + // Download file + let tmpfile: String = + match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) { + None => { + return Err("Could not create tempdir".to_string()); + } + Some(p) => p, + }; + let cache: PathBuf = match self.cache.as_ref() { + None => { + return Err("Could not create tempdir".to_string()); + } + Some(p) => p.path().to_path_buf(), + }; + + // open from host bridge + let mut reader = match self.host_bridge.open_file(entry.path()) { + Ok(reader) => reader, + Err(err) => { + return Err(format!("Failed to open bridged entry: {err}")); + } + }; + + let tempfile = cache.join(tmpfile); + + // write to file + let mut writer = match std::fs::File::create(tempfile.as_path()) { + Ok(writer) => writer, + Err(err) => { + return Err(format!("Failed to write file: {err}")); + } + }; + + let new_file_size = match std::io::copy(&mut reader, &mut writer) { + Err(err) => return Err(format!("Could not write file: {err}")), + Ok(size) => size, + }; + + // edit file + + let has_changed = self.edit_local_file(tempfile.as_path())?; + + if has_changed { + // report changes to remote + let mut reader = match std::fs::File::open(tempfile.as_path()) { + Ok(reader) => reader, + Err(err) => { + return Err(format!("Could not open file: {err}")); + } + }; + let mut writer = match self.host_bridge.create_file( + entry.path(), + &Metadata { + size: new_file_size, + ..Default::default() + }, + ) { + Ok(writer) => writer, + Err(err) => { + return Err(format!("Could not write file: {err}")); + } + }; + + if let Err(err) = std::io::copy(&mut reader, &mut writer) { + return Err(format!("Could not write file: {err}")); + } + + self.host_bridge + .finalize_write(writer) + .map_err(|err| format!("Could not write file: {err}"))?; + } + + Ok(()) + } + + fn edit_local_file(&mut self, path: &Path) -> Result { // Read first 2048 bytes or less from file to check if it is textual match OpenOptions::new().read(true).open(path) { Ok(mut f) => { @@ -90,6 +170,8 @@ impl FileTransferActivity { } // Lock ports assert!(self.app.lock_ports().is_ok()); + // Get current file modification time + let prev_mtime = self.get_localhost_mtime(path)?; // Open editor match edit::edit_file(path) { Ok(_) => self.log( @@ -117,7 +199,23 @@ impl FileTransferActivity { // Unlock ports assert!(self.app.unlock_ports().is_ok()); } - Ok(()) + let after_mtime = self.get_localhost_mtime(path)?; + + // return if file has changed + Ok(prev_mtime != after_mtime) + } + + fn get_localhost_mtime(&self, p: &Path) -> Result { + let attr = match std::fs::metadata(p) { + Ok(metadata) => metadata, + Err(err) => { + return Err(format!("Could not read file metadata: {}", err)); + } + }; + + Ok(Metadata::from(attr) + .modified + .unwrap_or(std::time::UNIX_EPOCH)) } /// Edit file on remote host @@ -138,7 +236,7 @@ impl FileTransferActivity { return Err(format!("Could not open file {file_name}: {err}")); } // Get current file modification time - let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) { + let prev_mtime: SystemTime = match self.host_bridge.stat(tmpfile.as_path()) { Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH), Err(err) => { return Err(format!( @@ -151,7 +249,7 @@ impl FileTransferActivity { // Edit file self.edit_local_file(tmpfile.as_path())?; // Get local fs entry - let tmpfile_entry: File = match self.host.stat(tmpfile.as_path()) { + let tmpfile_entry: File = match self.host_bridge.stat(tmpfile.as_path()) { Ok(e) => e, Err(err) => { return Err(format!( @@ -177,7 +275,7 @@ impl FileTransferActivity { ), ); // Get local fs entry - let tmpfile_entry = match self.host.stat(tmpfile.as_path()) { + let tmpfile_entry = match self.host_bridge.stat(tmpfile.as_path()) { Ok(e) => e, Err(err) => { return Err(format!( diff --git a/src/ui/activities/filetransfer/actions/exec.rs b/src/ui/activities/filetransfer/actions/exec.rs index 1c4a8f1..769820a 100644 --- a/src/ui/activities/filetransfer/actions/exec.rs +++ b/src/ui/activities/filetransfer/actions/exec.rs @@ -7,7 +7,7 @@ use super::{FileTransferActivity, LogLevel}; impl FileTransferActivity { pub(crate) fn action_local_exec(&mut self, input: String) { - match self.host.exec(input.as_str()) { + match self.host_bridge.exec(input.as_str()) { Ok(output) => { // Reload files self.log(LogLevel::Info, format!("\"{input}\": {output}")); diff --git a/src/ui/activities/filetransfer/actions/filter.rs b/src/ui/activities/filetransfer/actions/filter.rs index bf1d3bf..7d41523 100644 --- a/src/ui/activities/filetransfer/actions/filter.rs +++ b/src/ui/activities/filetransfer/actions/filter.rs @@ -40,7 +40,7 @@ impl FileTransferActivity { let filter = Filter::from_str(filter).unwrap(); match self.browser.tab() { - FileExplorerTab::Local => self.browser.local().iter_files(), + FileExplorerTab::HostBridge => self.browser.host_bridge().iter_files(), FileExplorerTab::Remote => self.browser.remote().iter_files(), _ => return vec![], } diff --git a/src/ui/activities/filetransfer/actions/find.rs b/src/ui/activities/filetransfer/actions/find.rs index 207b0ca..10038e5 100644 --- a/src/ui/activities/filetransfer/actions/find.rs +++ b/src/ui/activities/filetransfer/actions/find.rs @@ -24,8 +24,8 @@ impl FileTransferActivity { }; // Change directory match self.browser.tab() { - FileExplorerTab::FindLocal | FileExplorerTab::Local => { - self.local_changedir(path.as_path(), true) + FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { + self.host_bridge_changedir(path.as_path(), true) } FileExplorerTab::FindRemote | FileExplorerTab::Remote => { self.remote_changedir(path.as_path(), true) @@ -36,12 +36,16 @@ impl FileTransferActivity { pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) { let wrkdir: PathBuf = match self.browser.tab() { - FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(), - FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(), + FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { + self.remote().wrkdir.clone() + } + FileExplorerTab::FindRemote | FileExplorerTab::Remote => { + self.host_bridge().wrkdir.clone() + } }; match self.get_found_selected_entries() { SelectedFile::One(entry) => match self.browser.tab() { - FileExplorerTab::FindLocal | FileExplorerTab::Local => { + FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() && self.remote_file_exists(file_to_check.as_path()) @@ -66,7 +70,7 @@ impl FileTransferActivity { FileExplorerTab::FindRemote | FileExplorerTab::Remote => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() - && self.local_file_exists(file_to_check.as_path()) + && self.host_bridge_file_exists(file_to_check.as_path()) && !self.should_replace_file( opts.save_as.clone().unwrap_or_else(|| entry.name()), ) @@ -94,7 +98,7 @@ impl FileTransferActivity { } // Iter files match self.browser.tab() { - FileExplorerTab::FindLocal | FileExplorerTab::Local => { + FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { if self.config().get_prompt_on_file_replace() { // Check which file would be replaced let existing_files: Vec<&File> = entries @@ -131,7 +135,7 @@ impl FileTransferActivity { let existing_files: Vec<&File> = entries .iter() .filter(|x| { - self.local_file_exists( + self.host_bridge_file_exists( Self::file_to_check_many(x, dest_path.as_path()).as_path(), ) }) @@ -179,7 +183,7 @@ impl FileTransferActivity { fn remove_found_file(&mut self, entry: &File) { match self.browser.tab() { - FileExplorerTab::FindLocal | FileExplorerTab::Local => { + FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { self.local_remove_file(entry); } FileExplorerTab::FindRemote | FileExplorerTab::Remote => { @@ -224,7 +228,7 @@ impl FileTransferActivity { fn open_found_file(&mut self, entry: &File, with: Option<&str>) { match self.browser.tab() { - FileExplorerTab::FindLocal | FileExplorerTab::Local => { + FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { self.action_open_local_file(entry, with); } FileExplorerTab::FindRemote | FileExplorerTab::Remote => { diff --git a/src/ui/activities/filetransfer/actions/mkdir.rs b/src/ui/activities/filetransfer/actions/mkdir.rs index 85bc15f..2350c58 100644 --- a/src/ui/activities/filetransfer/actions/mkdir.rs +++ b/src/ui/activities/filetransfer/actions/mkdir.rs @@ -11,7 +11,10 @@ use super::{FileTransferActivity, LogLevel}; impl FileTransferActivity { pub(crate) fn action_local_mkdir(&mut self, input: String) { - match self.host.mkdir(PathBuf::from(input.as_str()).as_path()) { + match self + .host_bridge + .mkdir(PathBuf::from(input.as_str()).as_path()) + { Ok(_) => { // Reload files self.log(LogLevel::Info, format!("Created directory \"{input}\"")); diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index ad2b004..ba15c04 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -86,12 +86,12 @@ impl From> for SelectedFile { impl FileTransferActivity { /// Get local file entry pub(crate) fn get_local_selected_entries(&self) -> SelectedFile { - match self.get_selected_index(&Id::ExplorerLocal) { - SelectedFileIndex::One(idx) => SelectedFile::from(self.local().get(idx)), + match self.get_selected_index(&Id::ExplorerHostBridge) { + SelectedFileIndex::One(idx) => SelectedFile::from(self.host_bridge().get(idx)), SelectedFileIndex::Many(files) => { let files: Vec<&File> = files .iter() - .filter_map(|x| self.local().get(*x)) // Usize to Option + .filter_map(|x| self.host_bridge().get(*x)) // Usize to Option .collect(); SelectedFile::from(files) } diff --git a/src/ui/activities/filetransfer/actions/newfile.rs b/src/ui/activities/filetransfer/actions/newfile.rs index bc43f14..41b4c50 100644 --- a/src/ui/activities/filetransfer/actions/newfile.rs +++ b/src/ui/activities/filetransfer/actions/newfile.rs @@ -6,13 +6,15 @@ use std::fs::File as StdFile; use std::path::PathBuf; +use remotefs::fs::Metadata; + use super::{File, FileTransferActivity, LogLevel}; impl FileTransferActivity { pub(crate) fn action_local_newfile(&mut self, input: String) { // Check if file exists let mut file_exists: bool = false; - for file in self.local().iter_files_all() { + for file in self.host_bridge().iter_files_all() { if input == file.name() { file_exists = true; } @@ -21,19 +23,35 @@ impl FileTransferActivity { self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",)); return; } + // Create file let file_path: PathBuf = PathBuf::from(input.as_str()); - if let Err(err) = self.host.open_file_write(file_path.as_path()) { + let writer = match self + .host_bridge + .create_file(file_path.as_path(), &Metadata::default()) + { + Ok(f) => f, + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not create file \"{}\": {}", file_path.display(), err), + ); + return; + } + }; + // finalize write + if let Err(err) = self.host_bridge.finalize_write(writer) { self.log_and_alert( LogLevel::Error, - format!("Could not create file \"{}\": {}", file_path.display(), err), - ); - } else { - self.log( - LogLevel::Info, - format!("Created file \"{}\"", file_path.display()), + format!("Could not write file \"{}\": {}", file_path.display(), err), ); + return; } + + self.log( + LogLevel::Info, + format!("Created file \"{}\"", file_path.display()), + ); } pub(crate) fn action_remote_newfile(&mut self, input: String) { @@ -57,7 +75,7 @@ impl FileTransferActivity { } Ok(tfile) => { // Stat tempfile - let local_file: File = match self.host.stat(tfile.path()) { + let local_file: File = match self.host_bridge.stat(tfile.path()) { Err(err) => { self.log_and_alert( LogLevel::Error, diff --git a/src/ui/activities/filetransfer/actions/open.rs b/src/ui/activities/filetransfer/actions/open.rs index 6e699a7..51589ca 100644 --- a/src/ui/activities/filetransfer/actions/open.rs +++ b/src/ui/activities/filetransfer/actions/open.rs @@ -35,7 +35,11 @@ impl FileTransferActivity { /// Perform open lopcal file pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) { - self.open_path_with(entry.path(), open_with); + if self.host_bridge.is_localhost() { + self.open_path_with(entry.path(), open_with); + } else { + self.open_bridged_file(entry, open_with); + } } /// Open remote file. The file is first downloaded to a temporary directory on localhost @@ -104,6 +108,57 @@ impl FileTransferActivity { .for_each(|x| self.action_open_remote_file(x, Some(with))); } + fn open_bridged_file(&mut self, entry: &File, open_with: Option<&str>) { + // Download file + let tmpfile: String = + match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) { + None => { + self.log(LogLevel::Error, String::from("Could not create tempdir")); + return; + } + Some(p) => p, + }; + let cache: PathBuf = match self.cache.as_ref() { + None => { + self.log(LogLevel::Error, String::from("Could not create tempdir")); + return; + } + Some(p) => p.path().to_path_buf(), + }; + + let tmpfile = cache.join(tmpfile); + + // open from host bridge + let mut reader = match self.host_bridge.open_file(entry.path()) { + Ok(reader) => reader, + Err(err) => { + self.log( + LogLevel::Error, + format!("Failed to open bridged entry: {err}"), + ); + return; + } + }; + + // write to file + let mut writer = match std::fs::File::create(tmpfile.as_path()) { + Ok(writer) => writer, + Err(err) => { + self.log(LogLevel::Error, format!("Failed to create file: {err}")); + return; + } + }; + + if let Err(err) = std::io::copy(&mut reader, &mut writer) { + self.log(LogLevel::Error, format!("Failed to write file: {err}")); + return; + } + + if tmpfile.exists() { + self.open_path_with(tmpfile.as_path(), open_with); + } + } + /// Common function which opens a path with default or specified program. fn open_path_with(&mut self, p: &Path, with: Option<&str>) { // Open file diff --git a/src/ui/activities/filetransfer/actions/rename.rs b/src/ui/activities/filetransfer/actions/rename.rs index a80dffd..eb37191 100644 --- a/src/ui/activities/filetransfer/actions/rename.rs +++ b/src/ui/activities/filetransfer/actions/rename.rs @@ -51,7 +51,7 @@ impl FileTransferActivity { } fn local_rename_file(&mut self, entry: &File, dest: &Path) { - match self.host.rename(entry, dest) { + match self.host_bridge.rename(entry, dest) { Ok(_) => { self.log( LogLevel::Info, diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index e0c4024..b93c6f0 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -93,12 +93,12 @@ impl FileTransferActivity { } fn remote_recv_file(&mut self, opts: TransferOpts) { - let wrkdir: PathBuf = self.local().wrkdir.clone(); + let wrkdir: PathBuf = self.host_bridge().wrkdir.clone(); match self.get_remote_selected_entries() { SelectedFile::One(entry) => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() - && self.local_file_exists(file_to_check.as_path()) + && self.host_bridge_file_exists(file_to_check.as_path()) && !self .should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name())) { @@ -129,7 +129,7 @@ impl FileTransferActivity { let existing_files: Vec<&File> = entries .iter() .filter(|x| { - self.local_file_exists( + self.host_bridge_file_exists( Self::file_to_check_many(x, dest_path.as_path()).as_path(), ) }) diff --git a/src/ui/activities/filetransfer/actions/scan.rs b/src/ui/activities/filetransfer/actions/scan.rs index c187bf5..9cb052b 100644 --- a/src/ui/activities/filetransfer/actions/scan.rs +++ b/src/ui/activities/filetransfer/actions/scan.rs @@ -6,8 +6,8 @@ use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab; impl FileTransferActivity { pub(crate) fn action_scan(&mut self, p: &Path) -> Result, String> { match self.browser.tab() { - FileExplorerTab::Local | FileExplorerTab::FindLocal => self - .host + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => self + .host_bridge .list_dir(p) .map_err(|e| format!("Failed to list directory: {}", e)), FileExplorerTab::Remote | FileExplorerTab::FindRemote => self diff --git a/src/ui/activities/filetransfer/actions/submit.rs b/src/ui/activities/filetransfer/actions/submit.rs index 5bcf5ef..ccead4a 100644 --- a/src/ui/activities/filetransfer/actions/submit.rs +++ b/src/ui/activities/filetransfer/actions/submit.rs @@ -19,7 +19,7 @@ impl FileTransferActivity { } else if entry.metadata().symlink.is_some() { // Stat file let symlink = entry.metadata().symlink.as_ref().unwrap(); - let stat_file = match self.host.stat(symlink.as_path()) { + let stat_file = match self.host_bridge.stat(symlink.as_path()) { Ok(e) => e, Err(err) => { warn!( diff --git a/src/ui/activities/filetransfer/actions/symlink.rs b/src/ui/activities/filetransfer/actions/symlink.rs index 679b6fb..07bc831 100644 --- a/src/ui/activities/filetransfer/actions/symlink.rs +++ b/src/ui/activities/filetransfer/actions/symlink.rs @@ -9,11 +9,10 @@ use super::{FileTransferActivity, LogLevel, SelectedFile}; impl FileTransferActivity { /// Create symlink on localhost - #[cfg(unix)] pub(crate) fn action_local_symlink(&mut self, name: String) { if let SelectedFile::One(entry) = self.get_local_selected_entries() { match self - .host + .host_bridge .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) { Ok(_) => { @@ -33,11 +32,6 @@ impl FileTransferActivity { } } - #[cfg(windows)] - pub(crate) fn action_local_symlink(&mut self, _name: String) { - self.mount_error("Symlinks are not supported on Windows hosts"); - } - /// Copy file on remote pub(crate) fn action_remote_symlink(&mut self, name: String) { if let SelectedFile::One(entry) = self.get_remote_selected_entries() { diff --git a/src/ui/activities/filetransfer/actions/walkdir.rs b/src/ui/activities/filetransfer/actions/walkdir.rs index b18ef1d..7716cc2 100644 --- a/src/ui/activities/filetransfer/actions/walkdir.rs +++ b/src/ui/activities/filetransfer/actions/walkdir.rs @@ -18,8 +18,16 @@ impl FileTransferActivity { pub(crate) fn action_walkdir_local(&mut self) -> Result, WalkdirError> { let mut acc = Vec::with_capacity(32_768); - self.walkdir(&mut acc, &self.host.pwd(), |activity, path| { - activity.host.list_dir(path).map_err(|e| e.to_string()) + let pwd = self + .host_bridge + .pwd() + .map_err(|e| WalkdirError::Error(e.to_string()))?; + + self.walkdir(&mut acc, &pwd, |activity, path| { + activity + .host_bridge + .list_dir(path) + .map_err(|e| e.to_string()) })?; Ok(acc) diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index 3fba3d8..84b269b 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -1551,8 +1551,8 @@ pub struct StatusBarLocal { impl StatusBarLocal { pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self { - let file_sorting = file_sorting_label(browser.local().file_sorting); - let hidden_files = hidden_files_label(browser.local().hidden_files_visible()); + let file_sorting = file_sorting_label(browser.host_bridge().file_sorting); + let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible()); Self { component: Span::default().spans(&[ TextSpan::new("File sorting: ").fg(sorting_color), diff --git a/src/ui/activities/filetransfer/fswatcher.rs b/src/ui/activities/filetransfer/fswatcher.rs index 472304d..ae09bf9 100644 --- a/src/ui/activities/filetransfer/fswatcher.rs +++ b/src/ui/activities/filetransfer/fswatcher.rs @@ -30,10 +30,10 @@ impl FileTransferActivity { Ok(Some(FsChange::Update(update))) => { debug!( "fs watcher reported an `Update` from {} to {}", - update.local().display(), + update.host_bridge().display(), update.remote().display() ); - self.upload_watched_file(update.local(), update.remote()); + self.upload_watched_file(update.host_bridge(), update.remote()); } Err(err) => { self.log( @@ -87,9 +87,9 @@ impl FileTransferActivity { } } - fn upload_watched_file(&mut self, local: &Path, remote: &Path) { - // stat local file - let entry = match self.host.stat(local) { + fn upload_watched_file(&mut self, host: &Path, remote: &Path) { + // stat host file + let entry = match self.host_bridge.stat(host) { Ok(e) => e, Err(err) => { self.log( @@ -105,8 +105,8 @@ impl FileTransferActivity { }; // send trace!( - "syncing local file {} with remote {}", - local.display(), + "syncing host file {} with remote {}", + host.display(), remote.display() ); let remote_path = remote.parent().unwrap_or_else(|| Path::new("/")); @@ -116,7 +116,7 @@ impl FileTransferActivity { LogLevel::Info, format!( "synched watched file {} with {}", - local.display(), + host.display(), remote.display() ), ); diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index a93a31c..5564b02 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -16,10 +16,10 @@ const FUZZY_SEARCH_THRESHOLD: u16 = 50; /// File explorer tab #[derive(Clone, Copy, PartialEq, Eq)] pub enum FileExplorerTab { - Local, + HostBridge, Remote, - FindLocal, // Find result tab - FindRemote, // Find result tab + FindHostBridge, // Find result tab + FindRemote, // Find result tab } /// Describes the explorer tab type @@ -31,10 +31,10 @@ pub enum FoundExplorerTab { /// Browser contains the browser options pub struct Browser { - local: FileExplorer, // Local File explorer state - remote: FileExplorer, // Remote File explorer state - found: Option, // File explorer for find result - tab: FileExplorerTab, // Current selected tab + host_bridge: FileExplorer, // Local File explorer state + remote: FileExplorer, // Remote File explorer state + found: Option, // File explorer for find result + tab: FileExplorerTab, // Current selected tab pub sync_browsing: bool, } @@ -42,30 +42,30 @@ impl Browser { /// Build a new `Browser` struct pub fn new(cli: &ConfigClient) -> Self { Self { - local: Self::build_local_explorer(cli), + host_bridge: Self::build_local_explorer(cli), remote: Self::build_remote_explorer(cli), found: None, - tab: FileExplorerTab::Local, + tab: FileExplorerTab::HostBridge, sync_browsing: false, } } pub fn explorer(&self) -> &FileExplorer { match self.tab { - FileExplorerTab::Local => &self.local, + FileExplorerTab::HostBridge => &self.host_bridge, FileExplorerTab::Remote => &self.remote, - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { self.found.as_ref().map(|x| &x.explorer).unwrap() } } } - pub fn local(&self) -> &FileExplorer { - &self.local + pub fn host_bridge(&self) -> &FileExplorer { + &self.host_bridge } - pub fn local_mut(&mut self) -> &mut FileExplorer { - &mut self.local + pub fn host_bridge_mut(&mut self) -> &mut FileExplorer { + &mut self.host_bridge } pub fn remote(&self) -> &FileExplorer { diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 1895c15..70ffb32 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -1,8 +1,6 @@ -// Locals use std::env; use std::path::{Path, PathBuf}; -// Ext use bytesize::ByteSize; use tuirealm::props::{ Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextSpan, @@ -11,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}; @@ -95,9 +93,9 @@ impl FileTransferActivity { env::set_var("EDITOR", self.config().get_text_editor()); } - /// Convert a path to absolute according to local explorer - pub(super) fn local_to_abs_path(&self, path: &Path) -> PathBuf { - path::absolutize(self.local().wrkdir.as_path(), path) + /// Convert a path to absolute according to host explorer + pub(super) fn host_bridge_to_abs_path(&self, path: &Path) -> PathBuf { + path::absolutize(self.host_bridge().wrkdir.as_path(), path) } /// Convert a path to absolute according to remote explorer @@ -107,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) => { @@ -217,9 +235,9 @@ impl FileTransferActivity { } } - /// Update local file list - pub(super) fn update_local_filelist(&mut self) { - self.reload_local_dir(); + /// Update host bridge file list + pub(super) fn update_host_bridge_filelist(&mut self) { + self.reload_host_bridge_dir(); // Get width let width = self .context_mut() @@ -228,29 +246,26 @@ 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, - fmt_path_elide_ex(self.local().wrkdir.as_path(), width, hostname.len() + 3) // 3 because of '/…/' + "{hostname}:{} ", + fmt_path_elide_ex( + self.host_bridge().wrkdir.as_path(), + width, + hostname.len() + 3 + ) // 3 because of '/…/' ); let files: Vec> = self - .local() + .host_bridge() .iter_files() - .map(|x| vec![TextSpan::from(self.local().fmt_file(x))]) + .map(|x| vec![TextSpan::from(self.host_bridge().fmt_file(x))]) .collect(); // Update content and title assert!(self .app .attr( - &Id::ExplorerLocal, + &Id::ExplorerHostBridge, Attribute::Content, AttrValue::Table(files) ) @@ -258,7 +273,7 @@ impl FileTransferActivity { assert!(self .app .attr( - &Id::ExplorerLocal, + &Id::ExplorerHostBridge, Attribute::Title, AttrValue::Title((hostname, Alignment::Left)) ) @@ -409,17 +424,19 @@ impl FileTransferActivity { self.browser.del_found(); // Restore tab let new_tab = match self.browser.tab() { - FileExplorerTab::FindLocal => FileExplorerTab::Local, + FileExplorerTab::FindHostBridge => FileExplorerTab::HostBridge, FileExplorerTab::FindRemote => FileExplorerTab::Remote, - _ => FileExplorerTab::Local, + _ => FileExplorerTab::HostBridge, }; // Give focus to new tab match new_tab { - FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()), + FileExplorerTab::HostBridge => { + assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()) + } FileExplorerTab::Remote => { assert!(self.app.active(&Id::ExplorerRemote).is_ok()) } - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { assert!(self.app.active(&Id::ExplorerFind).is_ok()) } } @@ -445,15 +462,21 @@ impl FileTransferActivity { pub(super) fn update_browser_file_list(&mut self) { match self.browser.tab() { - FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_local_filelist(), + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + self.update_host_bridge_filelist() + } FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(), } } pub(super) fn update_browser_file_list_swapped(&mut self) { match self.browser.tab() { - FileExplorerTab::Local | FileExplorerTab::FindLocal => self.update_remote_filelist(), - FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_local_filelist(), + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + self.update_remote_filelist() + } + FileExplorerTab::Remote | FileExplorerTab::FindRemote => { + self.update_host_bridge_filelist() + } } } } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 9ea44ea..0a96b00 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -31,8 +31,10 @@ use tuirealm::{Application, EventListenerCfg, NoUserEvent}; use super::{Activity, Context, ExitReason}; use crate::config::themes::Theme; use crate::explorer::{FileExplorer, FileSorting}; -use crate::filetransfer::{Builder, FileTransferParams}; -use crate::host::Localhost; +use crate::filetransfer::{ + FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder, +}; +use crate::host::HostBridge; use crate::system::config_client::ConfigClient; use crate::system::watcher::FsWatcher; @@ -47,7 +49,7 @@ enum Id { ErrorPopup, ExecPopup, ExplorerFind, - ExplorerLocal, + ExplorerHostBridge, ExplorerRemote, FatalPopup, FileInfoPopup, @@ -68,7 +70,7 @@ enum Id { ReplacingFilesListPopup, SaveAsPopup, SortingPopup, - StatusBarLocal, + StatusBarHostBridge, StatusBarRemote, SymlinkPopup, SyncBrowsingMkdirPopup, @@ -213,8 +215,8 @@ pub struct FileTransferActivity { app: Application, /// Whether should redraw UI redraw: bool, - /// Localhost bridge - host: Localhost, + /// Host bridge + host_bridge: Box, /// Remote host client client: Box, /// Browser @@ -229,15 +231,25 @@ pub struct FileTransferActivity { cache: Option, /// Fs watcher fswatcher: Option, - /// connected once - connected: bool, + /// host bridge connected + host_bridge_connected: bool, + /// remote connected once + remote_connected: bool, } impl FileTransferActivity { /// Instantiates a new FileTransferActivity - pub fn new(host: Localhost, params: &FileTransferParams, ticks: Duration) -> Self { + pub fn new( + host_bridge_params: HostBridgeParams, + remote_params: &FileTransferParams, + ticks: Duration, + ) -> 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(); + let enable_fs_watcher = host_bridge.is_localhost(); Self { exit_reason: None, context: None, @@ -247,8 +259,12 @@ impl FileTransferActivity { .default_input_listener(ticks), ), redraw: true, - host, - client: Builder::build(params.protocol, params.params.clone(), &config_client), + host_bridge, + client: RemoteFsBuilder::build( + remote_params.protocol, + remote_params.params.clone(), + &config_client, + ), browser: Browser::new(&config_client), log_records: VecDeque::with_capacity(256), // 256 events is enough I guess walkdir: WalkdirStates::default(), @@ -257,23 +273,22 @@ impl FileTransferActivity { Ok(d) => Some(d), Err(_) => None, }, - fswatcher: match FsWatcher::init(Duration::from_secs(5)) { - Ok(w) => Some(w), - Err(e) => { - error!("failed to initialize fs watcher: {}", e); - None - } + fswatcher: if enable_fs_watcher { + FsWatcher::init(Duration::from_secs(5)).ok() + } else { + None }, - connected: false, + host_bridge_connected, + remote_connected: false, } } - fn local(&self) -> &FileExplorer { - self.browser.local() + fn host_bridge(&self) -> &FileExplorer { + self.browser.host_bridge() } - fn local_mut(&mut self) -> &mut FileExplorer { - self.browser.local_mut() + fn host_bridge_mut(&mut self) -> &mut FileExplorer { + self.browser.host_bridge_mut() } fn remote(&self) -> &FileExplorer { @@ -361,7 +376,10 @@ impl Activity for FileTransferActivity { error!("Failed to enter raw mode: {}", err); } // Get files at current pwd - self.reload_local_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(); @@ -384,15 +402,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; } @@ -432,6 +469,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() } } diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index c536a0d..5fd5c47 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -2,13 +2,10 @@ //! //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall -// Locals -use std::fs::File as StdFile; -use std::io::{Read, Seek, Write}; +use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::Instant; -// Ext use bytesize::ByteSize; use remotefs::fs::{File, Metadata, ReadStream, UnixPex, Welcome, WriteStream}; use remotefs::{RemoteError, RemoteErrorType, RemoteResult}; @@ -26,10 +23,8 @@ const BUFSIZE: usize = 65535; enum TransferErrorReason { #[error("File transfer aborted")] Abrupted, - #[error("Failed to seek file: {0}")] - CouldNotRewind(std::io::Error), - #[error("I/O error on localhost: {0}")] - LocalIoError(std::io::Error), + #[error("I/O error on host_bridgehost: {0}")] + HostIoError(std::io::Error), #[error("Host error: {0}")] HostError(HostError), #[error("I/O error on remote: {0}")] @@ -50,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 = 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 = 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 = 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; } @@ -94,7 +131,7 @@ impl FileTransferActivity { self.umount_wait(); self.reload_remote_dir(); // Update file lists - self.update_local_filelist(); + self.update_host_bridge_filelist(); self.update_remote_filelist(); } Err(err) => { @@ -124,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 @@ -149,35 +186,48 @@ impl FileTransferActivity { } } - /// Reload local directory entries and update browser - pub(super) fn reload_local_dir(&mut self) { - self.mount_blocking_wait("Loading local directory..."); + /// Reload host_bridge directory entries and update browser + pub(super) fn reload_host_bridge_dir(&mut self) { + if !self.host_bridge_connected { + return; + } - let wrkdir: PathBuf = self.host.pwd(); + self.mount_blocking_wait("Loading host bridge directory..."); - let res = self.local_scan(wrkdir.as_path()); + 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()); self.umount_wait(); match res { Ok(_) => { - self.local_mut().wrkdir = wrkdir; + self.host_bridge_mut().wrkdir = wrkdir; } Err(err) => { self.log_and_alert( LogLevel::Error, - format!("Could not scan current local directory: {err}"), + format!("Could not scan current host bridge directory: {err}"), ); } } } - /// Scan current local directory - fn local_scan(&mut self, path: &Path) -> Result<(), HostError> { - match self.host.list_dir(path) { + /// Scan current host bridge directory + fn host_bridge_scan(&mut self, path: &Path) -> Result<(), HostError> { + match self.host_bridge.list_dir(path) { Ok(files) => { // Set files and sort (sorting is implicit) - self.local_mut().set_files(files); + self.host_bridge_mut().set_files(files); Ok(()) } @@ -270,7 +320,7 @@ impl FileTransferActivity { // Reset states self.transfer.reset(); // Calculate total size of transfer - let total_transfer_size: usize = self.get_total_transfer_size_local(entry); + let total_transfer_size: usize = self.get_total_transfer_size_host(entry); self.transfer.full.init(total_transfer_size); // Mount progress bar self.mount_progress_bar(format!("Uploading {}…", entry.path().display())); @@ -292,7 +342,7 @@ impl FileTransferActivity { // Calculate total size of transfer let total_transfer_size: usize = entries .iter() - .map(|x| self.get_total_transfer_size_local(x)) + .map(|x| self.get_total_transfer_size_host(x)) .sum(); self.transfer.full.init(total_transfer_size); // Mount progress bar @@ -358,7 +408,7 @@ impl FileTransferActivity { } } // Get files in dir - match self.host.list_dir(entry.path()) { + match self.host_bridge.list_dir(entry.path()) { Ok(entries) => { // Iterate over files for entry in entries.iter() { @@ -433,17 +483,17 @@ impl FileTransferActivity { result } - /// Send local file and write it to remote path + /// Send host_bridge file and write it to remote path fn filetransfer_send_one( &mut self, - local: &File, + host_bridge: &File, remote: &Path, file_name: String, ) -> Result<(), TransferErrorReason> { // Sync file size and attributes before transfer let metadata = self - .host - .stat(local.path.as_path()) + .host_bridge + .stat(host_bridge.path.as_path()) .map_err(TransferErrorReason::HostError) .map(|x| x.metadata().clone())?; @@ -452,22 +502,30 @@ impl FileTransferActivity { LogLevel::Info, format!( "file {} won't be transferred since hasn't changed", - local.path().display() + host_bridge.path().display() ), ); self.transfer.full.update_progress(metadata.size as usize); return Ok(()); } // Upload file - // Try to open local file - match self.host.open_file_read(local.path.as_path()) { - Ok(fhnd) => match self.client.create(remote, &metadata) { - Ok(rhnd) => { - self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd) - } - Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { - self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd) - } + // Try to open host_bridge file + match self.host_bridge.open_file(host_bridge.path.as_path()) { + Ok(host_bridge_read) => match self.client.create(remote, &metadata) { + Ok(rhnd) => self.filetransfer_send_one_with_stream( + host_bridge, + remote, + file_name, + host_bridge_read, + rhnd, + ), + Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => self + .filetransfer_send_one_wno_stream( + host_bridge, + remote, + file_name, + host_bridge_read, + ), Err(err) => Err(TransferErrorReason::FileTransferError(err)), }, Err(err) => Err(TransferErrorReason::HostError(err)), @@ -477,20 +535,21 @@ impl FileTransferActivity { /// Send file to remote using stream fn filetransfer_send_one_with_stream( &mut self, - local: &File, + host: &File, remote: &Path, file_name: String, - mut reader: StdFile, + mut reader: Box, mut writer: WriteStream, ) -> Result<(), TransferErrorReason> { // Write file - let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; + let file_size = self + .host_bridge + .stat(host.path()) + .map_err(TransferErrorReason::HostError) + .map(|x| x.metadata().size as usize)?; // Init transfer self.transfer.partial.init(file_size); - // rewind - if let Err(err) = reader.rewind() { - return Err(TransferErrorReason::CouldNotRewind(err)); - } + // Write remote file let mut total_bytes_written: usize = 0; let mut last_progress_val: f64 = 0.0; @@ -535,7 +594,7 @@ impl FileTransferActivity { } } Err(err) => { - return Err(TransferErrorReason::LocalIoError(err)); + return Err(TransferErrorReason::HostIoError(err)); } }; // Increase progress @@ -561,14 +620,14 @@ impl FileTransferActivity { return Err(TransferErrorReason::Abrupted); } // set stat - if let Err(err) = self.client.setstat(remote, local.metadata().clone()) { + if let Err(err) = self.client.setstat(remote, host.metadata().clone()) { error!("failed to set stat for {}: {}", remote.display(), err); } self.log( LogLevel::Info, format!( "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - local.path.display(), + host.path.display(), remote.display(), fmt_millis(self.transfer.partial.started().elapsed()), ByteSize(self.transfer.partial.calc_bytes_per_second()), @@ -580,30 +639,31 @@ impl FileTransferActivity { /// Send an `File` to remote without using streams. fn filetransfer_send_one_wno_stream( &mut self, - local: &File, + host: &File, remote: &Path, file_name: String, - mut reader: StdFile, + reader: Box, ) -> Result<(), TransferErrorReason> { // Sync file size and attributes before transfer let metadata = self - .host - .stat(local.path.as_path()) + .host_bridge + .stat(host.path.as_path()) .map_err(TransferErrorReason::HostError) .map(|x| x.metadata().clone())?; // Write file - let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; + let file_size = self + .host_bridge + .stat(host.path()) + .map_err(TransferErrorReason::HostError) + .map(|x| x.metadata().size as usize)?; // Init transfer self.transfer.partial.init(file_size); - // rewind - if let Err(err) = reader.rewind() { - return Err(TransferErrorReason::CouldNotRewind(err)); - } + // Draw before self.update_progress_bar(format!("Uploading \"{file_name}\"…")); self.view(); // Send file - if let Err(err) = self.client.create_file(remote, &metadata, Box::new(reader)) { + if let Err(err) = self.client.create_file(remote, &metadata, reader) { return Err(TransferErrorReason::FileTransferError(err)); } // set stat @@ -621,7 +681,7 @@ impl FileTransferActivity { LogLevel::Info, format!( "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - local.path.display(), + host.path.display(), remote.display(), fmt_millis(self.transfer.partial.started().elapsed()), ByteSize(self.transfer.partial.calc_bytes_per_second()), @@ -636,15 +696,17 @@ impl FileTransferActivity { pub(super) fn filetransfer_recv( &mut self, payload: TransferPayload, - local_path: &Path, + host_bridge_path: &Path, dst_name: Option, ) -> Result<(), String> { let result = match payload { TransferPayload::Any(ref entry) => { - self.filetransfer_recv_any(entry, local_path, dst_name) + self.filetransfer_recv_any(entry, host_bridge_path, dst_name) + } + TransferPayload::File(ref file) => self.filetransfer_recv_file(file, host_bridge_path), + TransferPayload::Many(ref entries) => { + self.filetransfer_recv_many(entries, host_bridge_path) } - TransferPayload::File(ref file) => self.filetransfer_recv_file(file, local_path), - TransferPayload::Many(ref entries) => self.filetransfer_recv_many(entries, local_path), }; // Notify match &result { @@ -664,7 +726,7 @@ impl FileTransferActivity { fn filetransfer_recv_any( &mut self, entry: &File, - local_path: &Path, + host_path: &Path, dst_name: Option, ) -> Result<(), String> { // Reset states @@ -675,14 +737,18 @@ impl FileTransferActivity { // Mount progress bar self.mount_progress_bar(format!("Downloading {}…", entry.path().display())); // Receive - let result = self.filetransfer_recv_recurse(entry, local_path, dst_name); + let result = self.filetransfer_recv_recurse(entry, host_path, dst_name); // Umount progress bar self.umount_progress_bar(); result } /// Receive a single file from remote. - fn filetransfer_recv_file(&mut self, entry: &File, local_path: &Path) -> Result<(), String> { + fn filetransfer_recv_file( + &mut self, + entry: &File, + host_bridge_path: &Path, + ) -> Result<(), String> { // Reset states self.transfer.reset(); // Calculate total transfer size @@ -691,7 +757,7 @@ impl FileTransferActivity { // Mount progress bar self.mount_progress_bar(format!("Downloading {}…", entry.path.display())); // Receive - let result = self.filetransfer_recv_one(local_path, entry, entry.name()); + let result = self.filetransfer_recv_one(host_bridge_path, entry, entry.name()); // Umount progress bar self.umount_progress_bar(); // Return result @@ -728,7 +794,7 @@ impl FileTransferActivity { fn filetransfer_recv_recurse( &mut self, entry: &File, - local_path: &Path, + host_bridge_path: &Path, dst_name: Option, ) -> Result<(), String> { // Write popup @@ -736,32 +802,35 @@ impl FileTransferActivity { // Match entry let result: Result<(), String> = if entry.is_dir() { // Get dir name - let mut local_dir_path: PathBuf = PathBuf::from(local_path); + let mut host_bridge_dir_path: PathBuf = PathBuf::from(host_bridge_path); match dst_name { - Some(name) => local_dir_path.push(name), - None => local_dir_path.push(entry.name()), + Some(name) => host_bridge_dir_path.push(name), + None => host_bridge_dir_path.push(entry.name()), } - // Create directory on local - match self.host.mkdir_ex(local_dir_path.as_path(), true) { + // Create directory on host_bridge + match self + .host_bridge + .mkdir_ex(host_bridge_dir_path.as_path(), true) + { Ok(_) => { // Apply file mode to directory if let Err(err) = self - .host - .setstat(local_dir_path.as_path(), entry.metadata()) + .host_bridge + .setstat(host_bridge_dir_path.as_path(), entry.metadata()) { self.log( LogLevel::Error, format!( "Could not set stat to directory {:?} to \"{}\": {}", entry.metadata(), - local_dir_path.display(), + host_bridge_dir_path.display(), err ), ); } self.log( LogLevel::Info, - format!("Created directory \"{}\"", local_dir_path.display()), + format!("Created directory \"{}\"", host_bridge_dir_path.display()), ); // Get files in dir match self.client.list_dir(entry.path()) { @@ -773,10 +842,10 @@ impl FileTransferActivity { break; } // Receive entry; name is always None after first call - // Local path becomes local_dir_path + // Local path becomes host_bridge_dir_path self.filetransfer_recv_recurse( entry, - local_dir_path.as_path(), + host_bridge_dir_path.as_path(), None, )? } @@ -800,7 +869,7 @@ impl FileTransferActivity { LogLevel::Error, format!( "Failed to create directory \"{}\": {}", - local_dir_path.display(), + host_bridge_dir_path.display(), err ), ); @@ -808,39 +877,39 @@ impl FileTransferActivity { } } } else { - // Get local file - let mut local_file_path: PathBuf = PathBuf::from(local_path); - let local_file_name: String = match dst_name { + // Get host_bridge file + let mut host_bridge_file_path: PathBuf = PathBuf::from(host_bridge_path); + let host_bridge_file_name: String = match dst_name { Some(n) => n, None => entry.name(), }; - local_file_path.push(local_file_name.as_str()); + host_bridge_file_path.push(host_bridge_file_name.as_str()); // Download file if let Err(err) = - self.filetransfer_recv_one(local_file_path.as_path(), entry, file_name) + self.filetransfer_recv_one(host_bridge_file_path.as_path(), entry, file_name) { // If transfer was abrupted or there was an IO error on remote, remove file if matches!( err, - TransferErrorReason::Abrupted | TransferErrorReason::LocalIoError(_) + TransferErrorReason::Abrupted | TransferErrorReason::HostIoError(_) ) { // Stat file - match self.host.stat(local_file_path.as_path()) { + match self.host_bridge.stat(host_bridge_file_path.as_path()) { Err(err) => self.log( LogLevel::Error, format!( "Could not remove created file {}: {}", - local_file_path.display(), + host_bridge_file_path.display(), err ), ), Ok(entry) => { - if let Err(err) = self.host.remove(&entry) { + if let Err(err) = self.host_bridge.remove(&entry) { self.log( LogLevel::Error, format!( "Could not remove created file {}: {}", - local_file_path.display(), + host_bridge_file_path.display(), err ), ); @@ -853,8 +922,8 @@ impl FileTransferActivity { Ok(()) } }; - // Reload directory on local - self.reload_local_dir(); + // Reload directory on host_bridge + self.reload_host_bridge_dir(); // if aborted; show alert if self.transfer.aborted() { // Log abort @@ -866,15 +935,15 @@ impl FileTransferActivity { result } - /// Receive file from remote and write it to local path + /// Receive file from remote and write it to host_bridge path fn filetransfer_recv_one( &mut self, - local: &Path, + host_bridge: &Path, remote: &File, file_name: String, ) -> Result<(), TransferErrorReason> { // check if files are equal (in case, don't transfer) - if !self.has_local_file_changed(local, remote) { + if !self.has_host_bridge_file_changed(host_bridge, remote) { self.log( LogLevel::Info, format!( @@ -888,16 +957,20 @@ impl FileTransferActivity { return Ok(()); } - // Try to open local file - match self.host.open_file_write(local) { - Ok(local_file) => { + // Try to open host_bridge file + match self.host_bridge.create_file(host_bridge, &remote.metadata) { + Ok(writer) => { // Download file from remote match self.client.open(remote.path.as_path()) { Ok(rhnd) => self.filetransfer_recv_one_with_stream( - local, remote, file_name, rhnd, local_file, + host_bridge, + remote, + file_name, + rhnd, + writer, ), Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { - self.filetransfer_recv_one_wno_stream(local, remote, file_name) + self.filetransfer_recv_one_wno_stream(host_bridge, remote, file_name) } Err(err) => Err(TransferErrorReason::FileTransferError(err)), } @@ -909,16 +982,16 @@ impl FileTransferActivity { /// Receive an `File` from remote using stream fn filetransfer_recv_one_with_stream( &mut self, - local: &Path, + host_bridge: &Path, remote: &File, file_name: String, mut reader: ReadStream, - mut writer: StdFile, + mut writer: Box, ) -> Result<(), TransferErrorReason> { let mut total_bytes_written: usize = 0; // Init transfer self.transfer.partial.init(remote.metadata.size as usize); - // Write local file + // Write host_bridge file let mut last_progress_val: f64 = 0.0; let mut last_input_event_fetch: Option = None; // While the entire file hasn't been completely read, @@ -951,7 +1024,7 @@ impl FileTransferActivity { match writer.write(&buffer[delta..bytes_read]) { Ok(bytes) => delta += bytes, Err(err) => { - return Err(TransferErrorReason::LocalIoError(err)); + return Err(TransferErrorReason::HostIoError(err)); } } } @@ -984,14 +1057,20 @@ impl FileTransferActivity { if self.transfer.aborted() { return Err(TransferErrorReason::Abrupted); } + + // finalize write + self.host_bridge + .finalize_write(writer) + .map_err(TransferErrorReason::HostError)?; + // Apply file mode to file - if let Err(err) = self.host.setstat(local, remote.metadata()) { + if let Err(err) = self.host_bridge.setstat(host_bridge, remote.metadata()) { self.log( LogLevel::Error, format!( "Could not set stat to file {:?} to \"{}\": {}", remote.metadata(), - local.display(), + host_bridge.display(), err ), ); @@ -1002,25 +1081,26 @@ impl FileTransferActivity { format!( "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", remote.path.display(), - local.display(), + host_bridge.display(), fmt_millis(self.transfer.partial.started().elapsed()), ByteSize(self.transfer.partial.calc_bytes_per_second()), ), ); + Ok(()) } /// Receive an `File` from remote without using stream fn filetransfer_recv_one_wno_stream( &mut self, - local: &Path, + host_bridge: &Path, remote: &File, file_name: String, ) -> Result<(), TransferErrorReason> { - // Open local file + // Open host_bridge file let reader = self - .host - .open_file_write(local) + .host_bridge + .create_file(host_bridge, &remote.metadata) .map_err(TransferErrorReason::HostError) .map(Box::new)?; // Init transfer @@ -1043,13 +1123,13 @@ impl FileTransferActivity { self.update_progress_bar(format!("Downloading \"{file_name}\"")); self.view(); // Apply file mode to file - if let Err(err) = self.host.setstat(local, remote.metadata()) { + if let Err(err) = self.host_bridge.setstat(host_bridge, remote.metadata()) { self.log( LogLevel::Error, format!( "Could not set stat to file {:?} to \"{}\": {}", remote.metadata(), - local.display(), + host_bridge.display(), err ), ); @@ -1060,7 +1140,7 @@ impl FileTransferActivity { format!( "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", remote.path.display(), - local.display(), + host_bridge.display(), fmt_millis(self.transfer.partial.started().elapsed()), ByteSize(self.transfer.partial.calc_bytes_per_second()), ), @@ -1068,20 +1148,47 @@ impl FileTransferActivity { Ok(()) } - /// Change directory for local - pub(super) fn local_changedir(&mut self, path: &Path, push: bool) { + /// Change directory for host_bridge + pub(super) fn host_bridge_changedir(&mut self, path: &Path, push: bool) { // Get current directory - let prev_dir: PathBuf = self.local().wrkdir.clone(); + let prev_dir: PathBuf = self.host_bridge().wrkdir.clone(); // Change directory - match self.host.change_wrkdir(path) { + match self.host_bridge.change_wrkdir(path) { Ok(_) => { self.log( LogLevel::Info, - format!("Changed directory on local: {}", path.display()), + format!("Changed directory on host_bridge: {}", path.display()), ); // Push prev_dir to stack if push { - self.local_mut().pushd(prev_dir.as_path()) + 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 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) => { @@ -1152,14 +1259,14 @@ impl FileTransferActivity { // -- transfer sizes - /// Get total size of transfer for localhost - fn get_total_transfer_size_local(&mut self, entry: &File) -> usize { + /// Get total size of transfer for host_bridgehost + fn get_total_transfer_size_host(&mut self, entry: &File) -> usize { if entry.is_dir() { // List dir - match self.host.list_dir(entry.path()) { + match self.host_bridge.list_dir(entry.path()) { Ok(files) => files .iter() - .map(|x| self.get_total_transfer_size_local(x)) + .map(|x| self.get_total_transfer_size_host(x)) .sum(), Err(err) => { self.log( @@ -1206,23 +1313,23 @@ impl FileTransferActivity { // file changed - /// Check whether provided file has changed on local disk, compared to remote file - fn has_local_file_changed(&self, local: &Path, remote: &File) -> bool { + /// Check whether provided file has changed on host_bridge disk, compared to remote file + fn has_host_bridge_file_changed(&mut self, host_bridge: &Path, remote: &File) -> bool { // check if files are equal (in case, don't transfer) - if let Ok(local_file) = self.host.stat(local) { - local_file.metadata().modified != remote.metadata().modified - || local_file.metadata().size != remote.metadata().size + if let Ok(host_bridge_file) = self.host_bridge.stat(host_bridge) { + host_bridge_file.metadata().modified != remote.metadata().modified + || host_bridge_file.metadata().size != remote.metadata().size } else { true } } - /// Checks whether remote file has changed compared to local file - fn has_remote_file_changed(&mut self, remote: &Path, local_metadata: &Metadata) -> bool { + /// Checks whether remote file has changed compared to host_bridge file + fn has_remote_file_changed(&mut self, remote: &Path, host_bridge_metadata: &Metadata) -> bool { // check if files are equal (in case, don't transfer) if let Ok(remote_file) = self.client.stat(remote) { - local_metadata.modified != remote_file.metadata().modified - || local_metadata.size != remote_file.metadata().size + host_bridge_metadata.modified != remote_file.metadata().modified + || host_bridge_metadata.size != remote_file.metadata().size } else { true } @@ -1230,11 +1337,11 @@ impl FileTransferActivity { // -- file exist - pub(crate) fn local_file_exists(&mut self, p: &Path) -> bool { - self.host.file_exists(p) + pub(crate) fn host_bridge_file_exists(&mut self, p: &Path) -> bool { + self.host_bridge.exists(p).unwrap_or_default() } pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool { - self.client.stat(p).is_ok() + self.client.exists(p).unwrap_or_default() } } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 0e434ea..e60d732 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -40,14 +40,12 @@ impl FileTransferActivity { self.umount_chmod(); self.mount_blocking_wait("Applying new file mode…"); match self.browser.tab() { - #[cfg(unix)] - FileExplorerTab::Local => self.action_local_chmod(mode), - #[cfg(unix)] - FileExplorerTab::FindLocal => self.action_find_local_chmod(mode), + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge + if self.host_bridge.is_localhost() && cfg!(windows) => {} + FileExplorerTab::HostBridge => self.action_local_chmod(mode), + FileExplorerTab::FindHostBridge => self.action_find_local_chmod(mode), FileExplorerTab::Remote => self.action_remote_chmod(mode), FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode), - #[cfg(windows)] - FileExplorerTab::Local | FileExplorerTab::FindLocal => {} } self.umount_wait(); self.update_browser_file_list(); @@ -56,7 +54,7 @@ impl FileTransferActivity { self.umount_copy(); self.mount_blocking_wait("Copying file(s)…"); match self.browser.tab() { - FileExplorerTab::Local => self.action_local_copy(dest), + FileExplorerTab::HostBridge => self.action_local_copy(dest), FileExplorerTab::Remote => self.action_remote_copy(dest), _ => panic!("Found tab doesn't support COPY"), } @@ -68,7 +66,7 @@ impl FileTransferActivity { self.umount_symlink(); self.mount_blocking_wait("Creating symlink…"); match self.browser.tab() { - FileExplorerTab::Local => self.action_local_symlink(name), + FileExplorerTab::HostBridge => self.action_local_symlink(name), FileExplorerTab::Remote => self.action_remote_symlink(name), _ => panic!("Found tab doesn't support SYMLINK"), } @@ -80,9 +78,9 @@ impl FileTransferActivity { self.umount_radio_delete(); self.mount_blocking_wait("Removing file(s)…"); match self.browser.tab() { - FileExplorerTab::Local => self.action_local_delete(), + FileExplorerTab::HostBridge => self.action_local_delete(), FileExplorerTab::Remote => self.action_remote_delete(), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { // Get entry self.action_find_delete(); // Delete entries @@ -108,20 +106,20 @@ impl FileTransferActivity { self.umount_wait(); // Reload files match self.browser.tab() { - FileExplorerTab::Local => self.update_local_filelist(), + FileExplorerTab::HostBridge => self.update_host_bridge_filelist(), FileExplorerTab::Remote => self.update_remote_filelist(), - FileExplorerTab::FindLocal => self.update_local_filelist(), + FileExplorerTab::FindHostBridge => self.update_host_bridge_filelist(), FileExplorerTab::FindRemote => self.update_remote_filelist(), } } - TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Local => { + TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::HostBridge => { if let SelectedFile::One(entry) = self.get_local_selected_entries() { self.action_submit_local(entry); // Update file list if sync if self.browser.sync_browsing && self.browser.found().is_none() { self.update_remote_filelist(); } - self.update_local_filelist(); + self.update_host_bridge_filelist(); } } TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => { @@ -129,7 +127,7 @@ impl FileTransferActivity { self.action_submit_remote(entry); // Update file list if sync if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_local_filelist(); + self.update_host_bridge_filelist(); } self.update_remote_filelist(); } @@ -150,7 +148,7 @@ impl FileTransferActivity { self.umount_exec(); self.mount_blocking_wait(format!("Executing '{cmd}'…").as_str()); match self.browser.tab() { - FileExplorerTab::Local => self.action_local_exec(cmd), + FileExplorerTab::HostBridge => self.action_local_exec(cmd), FileExplorerTab::Remote => self.action_remote_exec(cmd), _ => panic!("Found tab doesn't support EXEC"), } @@ -160,7 +158,7 @@ impl FileTransferActivity { } TransferMsg::GoTo(dir) => { match self.browser.tab() { - FileExplorerTab::Local => self.action_change_local_dir(dir), + FileExplorerTab::HostBridge => self.action_change_local_dir(dir), FileExplorerTab::Remote => self.action_change_remote_dir(dir), _ => panic!("Found tab doesn't support GOTO"), } @@ -175,18 +173,18 @@ impl FileTransferActivity { } TransferMsg::GoToParentDirectory => { match self.browser.tab() { - FileExplorerTab::Local => { + FileExplorerTab::HostBridge => { self.action_go_to_local_upper_dir(); if self.browser.sync_browsing && self.browser.found().is_none() { self.update_remote_filelist(); } // Reload file list component - self.update_local_filelist() + self.update_host_bridge_filelist() } FileExplorerTab::Remote => { self.action_go_to_remote_upper_dir(); if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_local_filelist(); + self.update_host_bridge_filelist(); } // Reload file list component self.update_remote_filelist() @@ -196,18 +194,18 @@ impl FileTransferActivity { } TransferMsg::GoToPreviousDirectory => { match self.browser.tab() { - FileExplorerTab::Local => { + FileExplorerTab::HostBridge => { self.action_go_to_previous_local_dir(); if self.browser.sync_browsing && self.browser.found().is_none() { self.update_remote_filelist(); } // Reload file list component - self.update_local_filelist() + self.update_host_bridge_filelist() } FileExplorerTab::Remote => { self.action_go_to_previous_remote_dir(); if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_local_filelist(); + self.update_host_bridge_filelist(); } // Reload file list component self.update_remote_filelist() @@ -220,7 +218,7 @@ impl FileTransferActivity { self.mount_walkdir_wait(); // Find let res: Result, WalkdirError> = match self.browser.tab() { - FileExplorerTab::Local => self.action_walkdir_local(), + FileExplorerTab::HostBridge => self.action_walkdir_local(), FileExplorerTab::Remote => self.action_walkdir_remote(), _ => panic!("Trying to search for files, while already in a find result"), }; @@ -242,13 +240,13 @@ impl FileTransferActivity { Ok(files) => { // Get wrkdir let wrkdir = match self.browser.tab() { - FileExplorerTab::Local => self.local().wrkdir.clone(), + FileExplorerTab::HostBridge => self.host_bridge().wrkdir.clone(), _ => self.remote().wrkdir.clone(), }; // Create explorer and load files self.browser.set_found( match self.browser.tab() { - FileExplorerTab::Local => FoundExplorerTab::Local, + FileExplorerTab::HostBridge => FoundExplorerTab::Local, _ => FoundExplorerTab::Remote, }, files, @@ -261,16 +259,16 @@ impl FileTransferActivity { self.update_find_list(); // Initialize tab self.browser.change_tab(match self.browser.tab() { - FileExplorerTab::Local => FileExplorerTab::FindLocal, + FileExplorerTab::HostBridge => FileExplorerTab::FindHostBridge, FileExplorerTab::Remote => FileExplorerTab::FindRemote, - _ => FileExplorerTab::FindLocal, + _ => FileExplorerTab::FindHostBridge, }); } } } TransferMsg::Mkdir(dir) => { match self.browser.tab() { - FileExplorerTab::Local => self.action_local_mkdir(dir), + FileExplorerTab::HostBridge => self.action_local_mkdir(dir), FileExplorerTab::Remote => self.action_remote_mkdir(dir), _ => {} } @@ -280,7 +278,7 @@ impl FileTransferActivity { } TransferMsg::NewFile(name) => { match self.browser.tab() { - FileExplorerTab::Local => self.action_local_newfile(name), + FileExplorerTab::HostBridge => self.action_local_newfile(name), FileExplorerTab::Remote => self.action_remote_newfile(name), _ => {} } @@ -289,15 +287,17 @@ impl FileTransferActivity { self.update_browser_file_list() } TransferMsg::OpenFile => match self.browser.tab() { - FileExplorerTab::Local => self.action_open_local(), + FileExplorerTab::HostBridge => self.action_open_local(), FileExplorerTab::Remote => self.action_open_remote(), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => self.action_find_open(), + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { + self.action_find_open() + } }, TransferMsg::OpenFileWith(prog) => { match self.browser.tab() { - FileExplorerTab::Local => self.action_local_open_with(&prog), + FileExplorerTab::HostBridge => self.action_local_open_with(&prog), FileExplorerTab::Remote => self.action_remote_open_with(&prog), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { self.action_find_open_with(&prog) } } @@ -305,7 +305,7 @@ impl FileTransferActivity { } TransferMsg::OpenTextFile => { match self.browser.tab() { - FileExplorerTab::Local => self.action_edit_local_file(), + FileExplorerTab::HostBridge => self.action_edit_local_file(), FileExplorerTab::Remote => self.action_edit_remote_file(), _ => {} } @@ -316,7 +316,7 @@ impl FileTransferActivity { self.umount_rename(); self.mount_blocking_wait("Moving file(s)…"); match self.browser.tab() { - FileExplorerTab::Local => self.action_local_rename(dest), + FileExplorerTab::HostBridge => self.action_local_rename(dest), FileExplorerTab::Remote => self.action_remote_rename(dest), _ => {} } @@ -336,9 +336,9 @@ impl FileTransferActivity { TransferMsg::SaveFileAs(dest) => { self.umount_saveas(); match self.browser.tab() { - FileExplorerTab::Local => self.action_local_saveas(dest), + FileExplorerTab::HostBridge => self.action_local_saveas(dest), FileExplorerTab::Remote => self.action_remote_saveas(dest), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { // Get entry self.action_find_transfer(TransferOpts::default().save_as(Some(dest))); } @@ -352,9 +352,9 @@ impl FileTransferActivity { TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index), TransferMsg::TransferFile => { match self.browser.tab() { - FileExplorerTab::Local => self.action_local_send(), + FileExplorerTab::HostBridge => self.action_local_send(), FileExplorerTab::Remote => self.action_remote_recv(), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { self.action_find_transfer(TransferOpts::default()) } } @@ -371,8 +371,8 @@ impl FileTransferActivity { UiMsg::CloseChmodPopup => self.umount_chmod(), UiMsg::ChangeFileSorting(sorting) => { match self.browser.tab() { - FileExplorerTab::Local | FileExplorerTab::FindLocal => { - self.local_mut().sort_by(sorting); + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + self.host_bridge_mut().sort_by(sorting); self.refresh_local_status_bar(); } FileExplorerTab::Remote | FileExplorerTab::FindRemote => { @@ -384,22 +384,28 @@ impl FileTransferActivity { } UiMsg::ChangeTransferWindow => { let new_tab = match self.browser.tab() { - FileExplorerTab::Local if self.browser.found().is_some() => { + FileExplorerTab::HostBridge if self.browser.found().is_some() => { FileExplorerTab::FindRemote } - FileExplorerTab::FindLocal | FileExplorerTab::Local => FileExplorerTab::Remote, - FileExplorerTab::Remote if self.browser.found().is_some() => { - FileExplorerTab::FindLocal + FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { + FileExplorerTab::Remote + } + FileExplorerTab::Remote if self.browser.found().is_some() => { + FileExplorerTab::FindHostBridge + } + FileExplorerTab::FindRemote | FileExplorerTab::Remote => { + FileExplorerTab::HostBridge } - FileExplorerTab::FindRemote | FileExplorerTab::Remote => FileExplorerTab::Local, }; // Set focus match new_tab { - FileExplorerTab::Local => assert!(self.app.active(&Id::ExplorerLocal).is_ok()), + FileExplorerTab::HostBridge => { + assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()) + } FileExplorerTab::Remote => { assert!(self.app.active(&Id::ExplorerRemote).is_ok()) } - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { assert!(self.app.active(&Id::ExplorerFind).is_ok()) } } @@ -441,13 +447,13 @@ impl FileTransferActivity { let files = self.filter(&filter); // Get wrkdir let wrkdir = match self.browser.tab() { - FileExplorerTab::Local => self.local().wrkdir.clone(), + FileExplorerTab::HostBridge => self.host_bridge().wrkdir.clone(), _ => self.remote().wrkdir.clone(), }; // Create explorer and load files self.browser.set_found( match self.browser.tab() { - FileExplorerTab::Local => FoundExplorerTab::Local, + FileExplorerTab::HostBridge => FoundExplorerTab::Local, _ => FoundExplorerTab::Remote, }, files, @@ -458,9 +464,9 @@ impl FileTransferActivity { self.update_find_list(); // Initialize tab self.browser.change_tab(match self.browser.tab() { - FileExplorerTab::Local => FileExplorerTab::FindLocal, + FileExplorerTab::HostBridge => FileExplorerTab::FindHostBridge, FileExplorerTab::Remote => FileExplorerTab::FindRemote, - _ => FileExplorerTab::FindLocal, + _ => FileExplorerTab::FindHostBridge, }); } UiMsg::FuzzySearch(needle) => { @@ -471,7 +477,7 @@ impl FileTransferActivity { assert!(self.app.active(&Id::Log).is_ok()); } UiMsg::LogBackTabbed => { - assert!(self.app.active(&Id::ExplorerLocal).is_ok()); + assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()); } UiMsg::Quit => { self.disconnect_and_quit(); @@ -489,13 +495,15 @@ impl FileTransferActivity { UiMsg::ShowChmodPopup => { let selected_file = match self.browser.tab() { #[cfg(unix)] - FileExplorerTab::Local => self.get_local_selected_entries(), + FileExplorerTab::HostBridge => self.get_local_selected_entries(), #[cfg(unix)] - FileExplorerTab::FindLocal => self.get_found_selected_entries(), + FileExplorerTab::FindHostBridge => self.get_found_selected_entries(), FileExplorerTab::Remote => self.get_remote_selected_entries(), FileExplorerTab::FindRemote => self.get_found_selected_entries(), #[cfg(windows)] - FileExplorerTab::Local | FileExplorerTab::FindLocal => SelectedFile::None, + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + SelectedFile::None + } }; if let Some(mode) = selected_file.unix_pex() { self.mount_chmod( @@ -516,7 +524,7 @@ impl FileTransferActivity { UiMsg::ShowDeletePopup => self.mount_radio_delete(), UiMsg::ShowDisconnectPopup => self.mount_disconnect(), UiMsg::ShowExecPopup => self.mount_exec(), - UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Local => { + UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::HostBridge => { if let SelectedFile::One(file) = self.get_local_selected_entries() { self.mount_file_info(&file); } @@ -543,9 +551,9 @@ impl FileTransferActivity { UiMsg::ShowSaveAsPopup => self.mount_saveas(), UiMsg::ShowSymlinkPopup => { if match self.browser.tab() { - FileExplorerTab::Local => self.is_local_selected_one(), + FileExplorerTab::HostBridge => self.is_local_selected_one(), FileExplorerTab::Remote => self.is_remote_selected_one(), - FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => false, + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => false, } { // Only if only one entry is selected self.mount_symlink(); @@ -558,8 +566,8 @@ impl FileTransferActivity { UiMsg::ShowWatchedPathsList => self.action_show_watched_paths_list(), UiMsg::ShowWatcherPopup => self.action_show_radio_watch(), UiMsg::ToggleHiddenFiles => match self.browser.tab() { - FileExplorerTab::FindLocal | FileExplorerTab::Local => { - self.browser.local_mut().toggle_hidden_files(); + FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { + self.browser.host_bridge_mut().toggle_hidden_files(); self.refresh_local_status_bar(); self.update_browser_file_list(); } diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 29c55ee..4671f7b 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -44,7 +44,7 @@ impl FileTransferActivity { assert!(self .app .mount( - Id::ExplorerLocal, + Id::ExplorerHostBridge, Box::new(components::ExplorerLocal::new( "", &[], @@ -81,12 +81,12 @@ impl FileTransferActivity { self.refresh_local_status_bar(); self.refresh_remote_status_bar(); // Update components - self.update_local_filelist(); + self.update_host_bridge_filelist(); // self.update_remote_filelist(); // Global listener self.mount_global_listener(); // Give focus to local explorer - assert!(self.app.active(&Id::ExplorerLocal).is_ok()); + assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()); } // -- view @@ -141,7 +141,7 @@ impl FileTransferActivity { if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) { self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]); } else { - self.app.view(&Id::ExplorerLocal, f, tabs_chunks[0]); + self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]); } // @! Remote explorer (Find or default) if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) { @@ -152,7 +152,8 @@ impl FileTransferActivity { // Draw log box self.app.view(&Id::Log, f, bottom_chunks[1]); // Draw status bar - self.app.view(&Id::StatusBarLocal, f, status_bar_chunks[0]); + self.app + .view(&Id::StatusBarHostBridge, f, status_bar_chunks[0]); self.app.view(&Id::StatusBarRemote, f, status_bar_chunks[1]); // @! Draw popups if self.app.mounted(&Id::FatalPopup) { @@ -555,7 +556,7 @@ impl FileTransferActivity { pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) { // Get color let (bg, fg, hg) = match self.browser.tab() { - FileExplorerTab::Local | FileExplorerTab::FindLocal => ( + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => ( self.theme().transfer_local_explorer_background, self.theme().transfer_local_explorer_foreground, self.theme().transfer_local_explorer_highlighted, @@ -763,7 +764,7 @@ impl FileTransferActivity { pub(super) fn mount_file_sorting(&mut self) { let sorting_color = self.theme().transfer_status_sorting; let sorting: FileSorting = match self.browser.tab() { - FileExplorerTab::Local => self.local().get_file_sorting(), + FileExplorerTab::HostBridge => self.host_bridge().get_file_sorting(), FileExplorerTab::Remote => self.remote().get_file_sorting(), _ => return, }; @@ -901,7 +902,7 @@ impl FileTransferActivity { assert!(self .app .remount( - Id::StatusBarLocal, + Id::StatusBarHostBridge, Box::new(components::StatusBarLocal::new( &self.browser, sorting_color, diff --git a/src/ui/context.rs b/src/ui/context.rs index 46c0887..a9eb19c 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -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, + host_bridge_params: Option, + remote_params: Option, bookmarks_client: Option, 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,17 +97,16 @@ 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 - /// Set context error - pub fn set_error(&mut self, err: String) { - self.error = Some(err); - } - /// Get error message and remove it from the context pub fn error(&mut self) -> Option { self.error.take() diff --git a/src/utils/tty.rs b/src/utils/tty.rs index a810520..03c1bbe 100644 --- a/src/utils/tty.rs +++ b/src/utils/tty.rs @@ -2,11 +2,23 @@ //! //! `Utils` implements utilities functions to work with layouts +use tuirealm::terminal::TerminalBridge; + /// Read a secret from tty with customisable prompt -pub fn read_secret_from_tty(prompt: &str) -> std::io::Result> { - match rpassword::prompt_password(prompt) { +pub fn read_secret_from_tty( + terminal_bridge: &mut TerminalBridge, + prompt: impl ToString, +) -> std::io::Result> { + let _ = terminal_bridge.disable_raw_mode(); + let _ = terminal_bridge.leave_alternate_screen(); + let res = match rpassword::prompt_password(prompt) { Ok(p) if p.is_empty() => Ok(None), Ok(p) => Ok(Some(p)), Err(err) => Err(err), - } + }; + + let _ = terminal_bridge.enter_alternate_screen(); + let _ = terminal_bridge.enable_raw_mode(); + + res }