From 2caa0432dfd253b2faabe1f4034db55db4b28da5 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 3 May 2022 15:22:25 +0200 Subject: [PATCH] Remote directory path in authentication form and in bookmarks parameters --- CHANGELOG.md | 6 ++ src/config/bookmarks.rs | 42 +++++++++-- src/config/serialization.rs | 10 ++- src/ui/activities/auth/bookmarks.rs | 6 ++ src/ui/activities/auth/components/form.rs | 36 ++++++++++ src/ui/activities/auth/components/mod.rs | 4 +- src/ui/activities/auth/misc.rs | 4 +- src/ui/activities/auth/mod.rs | 3 + src/ui/activities/auth/update.rs | 16 +++-- src/ui/activities/auth/view.rs | 87 ++++++++++++++++++----- 10 files changed, 184 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 823e10d..bec71e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,12 @@ Released on FIXME: ``` If the password is stored in the bookmark, it will be used, otherwise you will be prompted to type the password in. +- **Remote directory path in authentication form and in bookmarks parameters**: + - It is now possible to configure the directory path you want to enter when you connect to the remote host from the authentication form + - This parameter can be stored into bookmarks as you already do with the other parameters + - You can find this field scrolling down in the authentication form +- **Enhancements**: + - Improved s3 auth form scrolling - Dependencies: - Updated `tui-realm` to `1.6.0` diff --git a/src/config/bookmarks.rs b/src/config/bookmarks.rs index 954be4f..9fe0798 100644 --- a/src/config/bookmarks.rs +++ b/src/config/bookmarks.rs @@ -30,6 +30,7 @@ use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; +use std::path::PathBuf; use std::str::FromStr; /// UserHosts contains all the hosts saved by the user in the data storage @@ -56,6 +57,8 @@ pub struct Bookmark { pub username: Option, /// Password is optional; base64, aes-128 encrypted password pub password: Option, + /// Remote folder to connect to + pub directory: Option, /// S3 params; optional. When used other fields are empty for sure pub s3: Option, } @@ -77,7 +80,8 @@ pub struct S3Params { impl From for Bookmark { fn from(params: FileTransferParams) -> Self { - let protocol: FileTransferProtocol = params.protocol; + let protocol = params.protocol; + let directory = params.entry_directory; // Create generic or others match params.params { ProtocolParams::Generic(params) => Self { @@ -86,6 +90,7 @@ impl From for Bookmark { port: Some(params.port), username: params.username, password: params.password, + directory, s3: None, }, ProtocolParams::AwsS3(params) => Self { @@ -94,6 +99,7 @@ impl From for Bookmark { port: None, username: None, password: None, + directory, s3: Some(S3Params::from(params)), }, } @@ -109,15 +115,18 @@ impl From for FileTransferParams { let params = AwsS3Params::from(params); Self::new(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) } - protocol => { + FileTransferProtocol::Ftp(_) + | FileTransferProtocol::Scp + | FileTransferProtocol::Sftp => { let params = GenericProtocolParams::default() .address(bookmark.address.unwrap_or_default()) .port(bookmark.port.unwrap_or(22)) .username(bookmark.username) .password(bookmark.password); - Self::new(protocol, ProtocolParams::Generic(params)) + Self::new(bookmark.protocol, ProtocolParams::Generic(params)) } } + .entry_directory(bookmark.directory) // Set entry directory } } @@ -187,6 +196,7 @@ mod tests { protocol: FileTransferProtocol::Sftp, username: Some(String::from("root")), password: Some(String::from("password")), + directory: Some(PathBuf::from("/tmp")), s3: None, }; let recent: Bookmark = Bookmark { @@ -195,6 +205,7 @@ mod tests { protocol: FileTransferProtocol::Scp, username: Some(String::from("admin")), password: Some(String::from("password")), + directory: Some(PathBuf::from("/home")), s3: None, }; let mut bookmarks: HashMap = HashMap::with_capacity(1); @@ -209,6 +220,10 @@ mod tests { assert_eq!(bookmark.protocol, FileTransferProtocol::Sftp); assert_eq!(bookmark.username.as_deref().unwrap(), "root"); assert_eq!(bookmark.password.as_deref().unwrap(), "password"); + assert_eq!( + bookmark.directory.as_deref().unwrap(), + std::path::Path::new("/tmp") + ); let bookmark: &Bookmark = hosts .recents .get(&String::from("ISO20201218T181432")) @@ -218,6 +233,10 @@ mod tests { assert_eq!(bookmark.protocol, FileTransferProtocol::Scp); assert_eq!(bookmark.username.as_deref().unwrap(), "admin"); assert_eq!(bookmark.password.as_deref().unwrap(), "password"); + assert_eq!( + bookmark.directory.as_deref().unwrap(), + std::path::Path::new("/home") + ); } #[test] @@ -228,13 +247,18 @@ mod tests { username: Some(String::from("root")), password: Some(String::from("omar")), }); - let params: FileTransferParams = FileTransferParams::new(FileTransferProtocol::Scp, params); + let params: FileTransferParams = FileTransferParams::new(FileTransferProtocol::Scp, params) + .entry_directory(Some(PathBuf::from("/home"))); let bookmark = Bookmark::from(params); assert_eq!(bookmark.protocol, FileTransferProtocol::Scp); assert_eq!(bookmark.address.as_deref().unwrap(), "127.0.0.1"); assert_eq!(bookmark.port.unwrap(), 10222); assert_eq!(bookmark.username.as_deref().unwrap(), "root"); assert_eq!(bookmark.password.as_deref().unwrap(), "omar"); + assert_eq!( + bookmark.directory.as_deref().unwrap(), + std::path::Path::new("/home") + ); assert!(bookmark.s3.is_none()); } @@ -269,10 +293,15 @@ mod tests { protocol: FileTransferProtocol::Sftp, username: Some(String::from("root")), password: Some(String::from("password")), + directory: Some(PathBuf::from("/tmp")), s3: None, }; let params = FileTransferParams::from(bookmark); assert_eq!(params.protocol, FileTransferProtocol::Sftp); + assert_eq!( + params.entry_directory.as_deref().unwrap(), + std::path::Path::new("/tmp") + ); let gparams = params.params.generic_params().unwrap(); assert_eq!(gparams.address.as_str(), "192.168.1.1"); assert_eq!(gparams.port, 22); @@ -288,6 +317,7 @@ mod tests { port: None, username: None, password: None, + directory: Some(PathBuf::from("/tmp")), s3: Some(S3Params { bucket: String::from("veeso"), region: Some(String::from("eu-west-1")), @@ -300,6 +330,10 @@ mod tests { }; let params = FileTransferParams::from(bookmark); assert_eq!(params.protocol, FileTransferProtocol::AwsS3); + assert_eq!( + params.entry_directory.as_deref().unwrap(), + std::path::Path::new("/tmp") + ); let gparams = params.params.s3_params().unwrap(); assert_eq!(gparams.bucket_name.as_str(), "veeso"); assert_eq!(gparams.region.as_deref().unwrap(), "eu-west-1"); diff --git a/src/config/serialization.rs b/src/config/serialization.rs index 8ead7ae..8abe92b 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -404,6 +404,10 @@ mod tests { assert_eq!(host.protocol, FileTransferProtocol::Sftp); assert_eq!(host.username.as_deref().unwrap(), "cvisintin"); assert_eq!(host.password.as_deref().unwrap(), "mysecret"); + assert_eq!( + host.directory.as_deref().unwrap(), + std::path::Path::new("/tmp") + ); let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap(); assert_eq!(host.address.as_deref().unwrap(), "51.23.67.12"); assert_eq!(host.port.unwrap(), 21); @@ -448,6 +452,7 @@ mod tests { protocol: FileTransferProtocol::Sftp, username: Some(String::from("root")), password: None, + directory: None, s3: None, }, ); @@ -459,6 +464,7 @@ mod tests { protocol: FileTransferProtocol::Sftp, username: Some(String::from("cvisintin")), password: Some(String::from("password")), + directory: Some(PathBuf::from("/tmp")), s3: None, }, ); @@ -470,6 +476,7 @@ mod tests { protocol: FileTransferProtocol::AwsS3, username: None, password: None, + directory: None, s3: Some(S3Params { bucket: "veeso".to_string(), region: Some("eu-west-1".to_string()), @@ -490,6 +497,7 @@ mod tests { protocol: FileTransferProtocol::Scp, username: Some(String::from("omar")), password: Some(String::from("aaa")), + directory: Some(PathBuf::from("/tmp")), s3: None, }, ); @@ -529,7 +537,7 @@ mod tests { let file_content: &str = r#" [bookmarks] raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" } - msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" } + msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret", directory = "/tmp" } aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" } [bookmarks.my-bucket] diff --git a/src/ui/activities/auth/bookmarks.rs b/src/ui/activities/auth/bookmarks.rs index 032c272..ea0deb4 100644 --- a/src/ui/activities/auth/bookmarks.rs +++ b/src/ui/activities/auth/bookmarks.rs @@ -171,6 +171,12 @@ impl AuthActivity { // Load parameters into components self.protocol = bookmark.protocol; self.mount_protocol(bookmark.protocol); + self.mount_remote_directory( + bookmark + .entry_directory + .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::Generic(params) => self.load_bookmark_generic_into_gui(params), diff --git a/src/ui/activities/auth/components/form.rs b/src/ui/activities/auth/components/form.rs index ab6050f..885da9f 100644 --- a/src/ui/activities/auth/components/form.rs +++ b/src/ui/activities/auth/components/form.rs @@ -112,6 +112,42 @@ impl Component for ProtocolRadio { } } +// -- remote directory + +#[derive(MockComponent)] +pub struct InputRemoteDirectory { + component: Input, +} + +impl InputRemoteDirectory { + pub fn new(remote_dir: &str, color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .placeholder("/home/foo", Style::default().fg(Color::Rgb(128, 128, 128))) + .title("Default remote directory", Alignment::Left) + .input_type(InputType::Text) + .value(remote_dir), + } + } +} + +impl Component for InputRemoteDirectory { + fn on(&mut self, ev: Event) -> Option { + handle_input_ev( + self, + ev, + Msg::Ui(UiMsg::RemoteDirectoryBlurDown), + Msg::Ui(UiMsg::RemoteDirectoryBlurUp), + ) + } +} + // -- address #[derive(MockComponent)] diff --git a/src/ui/activities/auth/components/mod.rs b/src/ui/activities/auth/components/mod.rs index 2c509ed..0c97d9c 100644 --- a/src/ui/activities/auth/components/mod.rs +++ b/src/ui/activities/auth/components/mod.rs @@ -37,8 +37,8 @@ pub use bookmarks::{ RecentsList, }; pub use form::{ - InputAddress, InputPassword, InputPort, InputS3AccessKey, InputS3Bucket, InputS3Endpoint, - InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, + InputAddress, InputPassword, InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, + InputS3Endpoint, InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, InputS3SessionToken, InputUsername, ProtocolRadio, RadioS3NewPathStyle, }; pub use popup::{ diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index e9d7169..1dc58a6 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -78,7 +78,7 @@ impl AuthActivity { Ok(FileTransferParams { protocol, params: ProtocolParams::Generic(params), - entry_directory: None, + entry_directory: self.get_input_remote_directory(), }) } @@ -91,7 +91,7 @@ impl AuthActivity { Ok(FileTransferParams { protocol: FileTransferProtocol::AwsS3, params: ProtocolParams::AwsS3(params), - entry_directory: None, + entry_directory: self.get_input_remote_directory(), }) } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index 816b657..399c1d3 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -66,6 +66,7 @@ pub enum Id { Protocol, QuitPopup, RecentsList, + RemoteDirectory, S3AccessKey, S3Bucket, S3Endpoint, @@ -125,6 +126,8 @@ pub enum UiMsg { ProtocolBlurDown, ProtocolBlurUp, RececentsListBlur, + RemoteDirectoryBlurDown, + RemoteDirectoryBlurUp, S3AccessKeyBlurDown, S3AccessKeyBlurUp, S3BucketBlurDown, diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index 1e7e2c7..797b9e7 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -178,7 +178,7 @@ impl AuthActivity { assert!(self.app.active(&Id::BookmarksList).is_ok()); } UiMsg::PasswordBlurDown => { - assert!(self.app.active(&Id::Protocol).is_ok()); + assert!(self.app.active(&Id::RemoteDirectory).is_ok()); } UiMsg::PasswordBlurUp => { assert!(self.app.active(&Id::Username).is_ok()); @@ -199,6 +199,15 @@ impl AuthActivity { .is_ok()); } UiMsg::ProtocolBlurUp => { + assert!(self.app.active(&Id::RemoteDirectory).is_ok()); + } + UiMsg::RececentsListBlur => { + assert!(self.app.active(&Id::BookmarksList).is_ok()); + } + UiMsg::RemoteDirectoryBlurDown => { + assert!(self.app.active(&Id::Protocol).is_ok()); + } + UiMsg::RemoteDirectoryBlurUp => { assert!(self .app .active(match self.input_mask() { @@ -207,9 +216,6 @@ impl AuthActivity { }) .is_ok()); } - UiMsg::RececentsListBlur => { - assert!(self.app.active(&Id::BookmarksList).is_ok()); - } UiMsg::S3BucketBlurDown => { assert!(self.app.active(&Id::S3Region).is_ok()); } @@ -259,7 +265,7 @@ impl AuthActivity { assert!(self.app.active(&Id::S3SecurityToken).is_ok()); } UiMsg::S3NewPathStyleBlurDown => { - assert!(self.app.active(&Id::Protocol).is_ok()); + assert!(self.app.active(&Id::RemoteDirectory).is_ok()); } UiMsg::S3NewPathStyleBlurUp => { assert!(self.app.active(&Id::S3SessionToken).is_ok()); diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 964e393..40972f8 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -31,6 +31,7 @@ use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolPa use crate::filetransfer::FileTransferParams; use crate::utils::ui::draw_area_in; +use std::path::PathBuf; use std::str::FromStr; use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::tui::widgets::Clear; @@ -67,6 +68,7 @@ impl AuthActivity { let default_protocol: FileTransferProtocol = self.context().config().get_default_protocol(); // Auth form self.mount_protocol(default_protocol); + self.mount_remote_directory(""); self.mount_address(""); self.mount_port(Self::get_default_port_for_protocol(default_protocol)); self.mount_username(""); @@ -165,6 +167,7 @@ impl AuthActivity { Constraint::Length(3), // region Constraint::Length(3), // profile Constraint::Length(3), // access_key + Constraint::Length(3), // remote directory ] .as_ref(), ) @@ -177,6 +180,7 @@ impl AuthActivity { Constraint::Length(3), // port Constraint::Length(3), // username Constraint::Length(3), // password + Constraint::Length(3), // remote directory ] .as_ref(), ) @@ -197,17 +201,18 @@ impl AuthActivity { // Render input mask match self.input_mask() { InputMask::AwsS3 => { - let s3_view_ids = self.get_s3_view(); - self.app.view(&s3_view_ids[0], f, input_mask[0]); - self.app.view(&s3_view_ids[1], f, input_mask[1]); - self.app.view(&s3_view_ids[2], f, input_mask[2]); - self.app.view(&s3_view_ids[3], f, input_mask[3]); + 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 => { - self.app.view(&Id::Address, f, input_mask[0]); - self.app.view(&Id::Port, f, input_mask[1]); - self.app.view(&Id::Username, f, input_mask[2]); - self.app.view(&Id::Password, f, input_mask[3]); + 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]); } } // Bookmark chunks @@ -563,6 +568,21 @@ impl AuthActivity { .is_ok()); } + pub(super) fn mount_remote_directory>(&mut self, entry_directory: S) { + let protocol_color = self.theme().auth_protocol; + assert!(self + .app + .remount( + Id::RemoteDirectory, + Box::new(components::InputRemoteDirectory::new( + entry_directory.as_ref(), + protocol_color + )), + vec![] + ) + .is_ok()); + } + pub(super) fn mount_address(&mut self, address: &str) { let addr_color = self.theme().auth_address; assert!(self @@ -754,6 +774,15 @@ impl AuthActivity { .new_path_style(new_path_style) } + pub(super) fn get_input_remote_directory(&self) -> Option { + match self.app.state(&Id::RemoteDirectory) { + Ok(State::One(StateValue::String(x))) if !x.is_empty() => { + Some(PathBuf::from(x.as_str())) + } + _ => None, + } + } + pub(super) fn get_input_addr(&self) -> String { match self.app.state(&Id::Address) { Ok(State::One(StateValue::String(x))) => x, @@ -913,26 +942,52 @@ impl AuthActivity { } } + /// Get the visible element in the generic params form, based on current focus + fn get_generic_params_view(&self) -> [Id; 4] { + match self.app.focus() { + Some(&Id::RemoteDirectory) => { + [Id::Port, Id::Username, Id::Password, Id::RemoteDirectory] + } + _ => [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] { match self.app.focus() { - Some(&Id::S3AccessKey) => [ + Some(&Id::S3AccessKey) => { + [Id::S3Region, Id::S3Endpoint, Id::S3Profile, Id::S3AccessKey] + } + Some(&Id::S3SecretAccessKey) => [ + Id::S3Endpoint, + Id::S3Profile, + Id::S3AccessKey, + Id::S3SecretAccessKey, + ], + Some(&Id::S3SecurityToken) => [ + Id::S3Profile, + Id::S3AccessKey, + Id::S3SecretAccessKey, + Id::S3SecurityToken, + ], + Some(&Id::S3SessionToken) => [ Id::S3AccessKey, Id::S3SecretAccessKey, Id::S3SecurityToken, Id::S3SessionToken, ], - Some( - &Id::S3SecretAccessKey - | &Id::S3SecurityToken - | &Id::S3SessionToken - | &Id::S3NewPathStyle, - ) => [ + Some(&Id::S3NewPathStyle) => [ Id::S3SecretAccessKey, Id::S3SecurityToken, Id::S3SessionToken, Id::S3NewPathStyle, ], + Some(&Id::RemoteDirectory) => [ + Id::S3SecurityToken, + Id::S3SessionToken, + Id::S3NewPathStyle, + Id::RemoteDirectory, + ], _ => [Id::S3Bucket, Id::S3Region, Id::S3Endpoint, Id::S3Profile], } }