Aws s3 support

This commit is contained in:
veeso
2021-08-26 11:24:13 +02:00
parent f31f58aa79
commit 1d09095ab9
37 changed files with 3458 additions and 973 deletions

View File

@@ -26,14 +26,15 @@
* SOFTWARE.
*/
// Locals
use super::{AuthActivity, FileTransferProtocol};
use super::{AuthActivity, FileTransferParams};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::environment;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{input::InputPropsBuilder, radio::RadioPropsBuilder};
use tuirealm::{Payload, PropsBuilder, Value};
use tuirealm::PropsBuilder;
impl AuthActivity {
/// ### del_bookmark
@@ -62,9 +63,7 @@ impl AuthActivity {
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.0, bookmark.1, bookmark.2, bookmark.3, bookmark.4,
);
self.load_bookmark_into_gui(bookmark);
}
}
}
@@ -74,20 +73,15 @@ impl AuthActivity {
///
/// Save current input fields as a bookmark
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
let (address, port, protocol, username, password) = self.get_input();
let params = match self.collect_host_params() {
Ok(p) => p,
Err(e) => {
self.mount_error(e);
return;
}
};
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
// Check if password must be saved
let password: Option<String> = match save_password {
true => match self
.view
.get_state(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
{
Some(Payload::One(Value::Usize(0))) => Some(password), // Yes
_ => None, // No such component / No
},
false => None,
};
bookmarks_cli.add_bookmark(name.clone(), address, port, protocol, username, password);
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
// Save bookmarks
self.write_bookmarks();
// Remove `name` from bookmarks if exists
@@ -122,9 +116,7 @@ impl AuthActivity {
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.0, bookmark.1, bookmark.2, bookmark.3, None,
);
self.load_bookmark_into_gui(bookmark);
}
}
}
@@ -134,9 +126,15 @@ impl AuthActivity {
///
/// Save current input fields as a "recent"
pub(super) fn save_recent(&mut self) {
let (address, port, protocol, username, _password) = self.get_input();
let params = match self.collect_host_params() {
Ok(p) => p,
Err(e) => {
self.mount_error(e);
return;
}
};
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
bookmarks_cli.add_recent(address, port, protocol, username);
bookmarks_cli.add_recent(params);
// Save bookmarks
self.write_bookmarks();
}
@@ -234,40 +232,66 @@ impl AuthActivity {
/// ### load_bookmark_into_gui
///
/// Load bookmark data into the gui components
fn load_bookmark_into_gui(
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) {
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
// Load parameters into components
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(bookmark.protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
match bookmark.params {
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
}
}
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
let props = InputPropsBuilder::from(props).with_value(addr).build();
let props = InputPropsBuilder::from(props)
.with_value(params.address.clone())
.build();
self.view.update(super::COMPONENT_INPUT_ADDR, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = InputPropsBuilder::from(props)
.with_value(port.to_string())
.with_value(params.port.to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PORT, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
let props = InputPropsBuilder::from(props).with_value(username).build();
let props = InputPropsBuilder::from(props)
.with_value(params.username.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
}
if let Some(password) = password {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props).with_value(password).build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props)
.with_value(params.password.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
}
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_BUCKET) {
let props = InputPropsBuilder::from(props)
.with_value(params.bucket_name.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_BUCKET, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_REGION) {
let props = InputPropsBuilder::from(props)
.with_value(params.region.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_REGION, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_PROFILE) {
let props = InputPropsBuilder::from(props)
.with_value(params.profile.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_S3_PROFILE, props);
}
}
}

View File

@@ -26,6 +26,7 @@
* SOFTWARE.
*/
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
impl AuthActivity {
/// ### protocol_opt_to_enum
@@ -36,6 +37,7 @@ impl AuthActivity {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
}
}
@@ -49,6 +51,7 @@ impl AuthActivity {
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
}
}
@@ -59,6 +62,7 @@ impl AuthActivity {
match protocol {
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
FileTransferProtocol::Ftp(_) => 21,
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
}
}
@@ -83,15 +87,24 @@ impl AuthActivity {
/// ### collect_host_params
///
/// Get input values from fields or return an error if fields are invalid
/// Collect host params as `FileTransferParams`
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
let (address, port, protocol, username, password): (
String,
u16,
FileTransferProtocol,
String,
String,
) = self.get_input();
let protocol: FileTransferProtocol = self.get_protocol();
match protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol),
protocol => self.collect_generic_host_params(protocol),
}
}
/// ### collect_generic_host_params
///
/// 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,
) -> Result<FileTransferParams, &'static str> {
let (address, port, username, password): (String, u16, String, String) =
self.get_generic_params_input();
if address.is_empty() {
return Err("Invalid host");
}
@@ -99,17 +112,42 @@ impl AuthActivity {
return Err("Invalid port");
}
Ok(FileTransferParams {
address,
port,
protocol,
username: match username.is_empty() {
true => None,
false => Some(username),
},
password: match password.is_empty() {
true => None,
false => Some(password),
},
params: ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(match username.is_empty() {
true => None,
false => Some(username),
})
.password(match password.is_empty() {
true => None,
false => Some(password),
}),
),
entry_directory: None,
})
}
/// ### collect_s3_host_params
///
/// 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,
protocol: FileTransferProtocol,
) -> Result<FileTransferParams, &'static str> {
let (bucket, region, profile): (String, String, Option<String>) =
self.get_s3_params_input();
if bucket.is_empty() {
return Err("Invalid bucket");
}
if region.is_empty() {
return Err("Invalid region");
}
Ok(FileTransferParams {
protocol,
params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
entry_directory: None,
})
}

View File

@@ -57,6 +57,9 @@ const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME";
const COMPONENT_INPUT_S3_BUCKET: &str = "INPUT_S3_BUCKET";
const COMPONENT_INPUT_S3_REGION: &str = "INPUT_S3_REGION";
const COMPONENT_INPUT_S3_PROFILE: &str = "INPUT_S3_PROFILE";
const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
@@ -163,6 +166,16 @@ impl AuthActivity {
fn theme(&self) -> &Theme {
self.context().theme_provider().theme()
}
/// ### input_mask
///
/// Get current input mask to show
fn input_mask(&self) -> InputMask {
match self.get_protocol() {
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
_ => InputMask::Generic,
}
}
}
impl Activity for AuthActivity {
@@ -261,3 +274,12 @@ impl Activity for AuthActivity {
}
}
}
/// ## InputMask
///
/// Auth form input mask
#[derive(Eq, PartialEq)]
enum InputMask {
Generic,
AwsS3,
}

View File

@@ -27,8 +27,9 @@
*/
// locals
use super::{
AuthActivity, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
AuthActivity, FileTransferProtocol, InputMask, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
COMPONENT_INPUT_S3_BUCKET, COMPONENT_INPUT_S3_PROFILE, COMPONENT_INPUT_S3_REGION,
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
@@ -53,54 +54,80 @@ impl Update for AuthActivity {
Some(msg) => match msg {
// Focus ( DOWN )
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR);
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET),
};
None
}
// -- generic mask (DOWN)
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 mask (DOWN)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_PROFILE);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Focus ( UP )
// -- generic (UP)
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR);
None
}
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 (UP)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_BUCKET);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD);
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE),
};
None
}
// Protocol - On Change
@@ -144,14 +171,20 @@ impl Update for AuthActivity {
// Enter
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_bookmark(*idx);
// Give focus to input password
self.view.active(COMPONENT_INPUT_PASSWORD);
// Give focus to input password (or to protocol if not generic)
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_recent(*idx);
// Give focus to input password
self.view.active(COMPONENT_INPUT_PASSWORD);
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
// Bookmark radio
@@ -320,7 +353,7 @@ impl Update for AuthActivity {
if key == &MSG_KEY_TAB =>
{
// Give focus to address
self.view.active(COMPONENT_INPUT_ADDR);
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Any <TAB>, go to bookmarks

View File

@@ -26,7 +26,9 @@
* SOFTWARE.
*/
// Locals
use super::{AuthActivity, Context, FileTransferProtocol};
use super::{AuthActivity, Context, FileTransferProtocol, InputMask};
use crate::filetransfer::params::ProtocolParams;
use crate::filetransfer::FileTransferParams;
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use crate::utils::ui::draw_area_in;
// Ext
@@ -109,7 +111,7 @@ impl AuthActivity {
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, protocol_color)
.with_title("Protocol", Alignment::Left)
.with_options(&["SFTP", "SCP", "FTP", "FTPS"])
.with_options(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
.with_value(Self::protocol_enum_to_opt(default_protocol))
.rewind(true)
.build(),
@@ -163,6 +165,39 @@ impl AuthActivity {
.build(),
)),
);
// Bucket
self.view.mount(
super::COMPONENT_INPUT_S3_BUCKET,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(addr_color)
.with_borders(Borders::ALL, BorderType::Rounded, addr_color)
.with_label("Bucket name", Alignment::Left)
.build(),
)),
);
// Region
self.view.mount(
super::COMPONENT_INPUT_S3_REGION,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(port_color)
.with_borders(Borders::ALL, BorderType::Rounded, port_color)
.with_label("Region", Alignment::Left)
.build(),
)),
);
// Profile
self.view.mount(
super::COMPONENT_INPUT_S3_PROFILE,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(username_color)
.with_borders(Borders::ALL, BorderType::Rounded, username_color)
.with_label("Profile", Alignment::Left)
.build(),
)),
);
// Version notice
if let Some(version) = self
.context()
@@ -240,20 +275,43 @@ 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(3), // host
Constraint::Length(3), // port
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // footer
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(3), // footer
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(chunks[0]);
// Input mask chunks
let input_mask = match self.input_mask() {
InputMask::AwsS3 => Layout::default()
.constraints(
[
Constraint::Length(3), // bucket
Constraint::Length(3), // region
Constraint::Length(3), // profile
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
InputMask::Generic => Layout::default()
.constraints(
[
Constraint::Length(3), // host
Constraint::Length(3), // port
Constraint::Length(3), // username
Constraint::Length(3), // password
]
.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())
@@ -269,16 +327,29 @@ impl AuthActivity {
.render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]);
self.view
.render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]);
// Render input mask
match self.input_mask() {
InputMask::AwsS3 => {
self.view
.render(super::COMPONENT_INPUT_S3_BUCKET, f, input_mask[0]);
self.view
.render(super::COMPONENT_INPUT_S3_REGION, f, input_mask[1]);
self.view
.render(super::COMPONENT_INPUT_S3_PROFILE, f, input_mask[2]);
}
InputMask::Generic => {
self.view
.render(super::COMPONENT_INPUT_ADDR, f, input_mask[0]);
self.view
.render(super::COMPONENT_INPUT_PORT, f, input_mask[1]);
self.view
.render(super::COMPONENT_INPUT_USERNAME, f, input_mask[2]);
self.view
.render(super::COMPONENT_INPUT_PASSWORD, f, input_mask[3]);
}
}
self.view
.render(super::COMPONENT_INPUT_ADDR, f, auth_chunks[4]);
self.view
.render(super::COMPONENT_INPUT_PORT, f, auth_chunks[5]);
self.view
.render(super::COMPONENT_INPUT_USERNAME, f, auth_chunks[6]);
self.view
.render(super::COMPONENT_INPUT_PASSWORD, f, auth_chunks[7]);
self.view
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[8]);
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[5]);
// Bookmark chunks
self.view
.render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]);
@@ -388,19 +459,13 @@ impl AuthActivity {
.bookmarks_list
.iter()
.map(|x| {
let entry: (String, u16, FileTransferProtocol, String, _) = self
.bookmarks_client
.as_ref()
.unwrap()
.get_bookmark(x)
.unwrap();
format!(
"{} ({}://{}@{}:{})",
Self::fmt_bookmark(
x,
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
self.bookmarks_client
.as_ref()
.unwrap()
.get_bookmark(x)
.unwrap(),
)
})
.collect();
@@ -426,19 +491,12 @@ impl AuthActivity {
.recents_list
.iter()
.map(|x| {
let entry: (String, u16, FileTransferProtocol, String) = self
.bookmarks_client
.as_ref()
.unwrap()
.get_recent(x)
.unwrap();
format!(
"{}://{}@{}:{}",
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
Self::fmt_recent(
self.bookmarks_client
.as_ref()
.unwrap()
.get_recent(x)
.unwrap(),
)
})
.collect();
@@ -743,16 +801,32 @@ impl AuthActivity {
self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
}
/// ### get_input
/// ### get_protocol
///
/// Get protocol from view
pub(super) fn get_protocol(&self) -> FileTransferProtocol {
self.get_input_protocol()
}
/// ### get_generic_params
///
/// Collect input values from view
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
pub(super) fn get_generic_params_input(&self) -> (String, u16, String, String) {
let addr: String = self.get_input_addr();
let port: u16 = self.get_input_port();
let protocol: FileTransferProtocol = self.get_input_protocol();
let username: String = self.get_input_username();
let password: String = self.get_input_password();
(addr, port, protocol, username, password)
(addr, port, username, password)
}
/// ### get_s3_params_input
///
/// Collect s3 input values from view
pub(super) fn get_s3_params_input(&self) -> (String, String, Option<String>) {
let bucket: String = self.get_input_s3_bucket();
let region: String = self.get_input_s3_region();
let profile: Option<String> = self.get_input_s3_profile();
(bucket, region, profile)
}
pub(super) fn get_input_addr(&self) -> String {
@@ -792,4 +866,75 @@ impl AuthActivity {
_ => String::new(),
}
}
pub(super) fn get_input_s3_bucket(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_S3_BUCKET) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_s3_region(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_S3_REGION) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_s3_profile(&self) -> Option<String> {
match self.view.get_state(super::COMPONENT_INPUT_S3_PROFILE) {
Some(Payload::One(Value::Str(x))) => match x.is_empty() {
true => None,
false => Some(x),
},
_ => None,
}
}
/// ### input_mask_size
///
/// Returns the input mask size based on current input mask
pub(super) fn input_mask_size(&self) -> u16 {
match self.input_mask() {
InputMask::AwsS3 => 9,
InputMask::Generic => 12,
}
}
/// ### fmt_bookmark
///
/// Format bookmark to display on ui
fn fmt_bookmark(name: &str, b: FileTransferParams) -> String {
let addr: String = Self::fmt_recent(b);
format!("{} ({})", name, addr)
}
/// ### fmt_recent
///
/// Format recent connection to display on ui
fn fmt_recent(b: FileTransferParams) -> String {
let protocol: String = b.protocol.to_string().to_lowercase();
match b.params {
ProtocolParams::AwsS3(s3) => {
let profile: String = match s3.profile {
Some(p) => format!("[{}]", p),
None => String::default(),
};
format!(
"{}://{} ({}) {}",
protocol, s3.bucket_name, s3.region, profile
)
}
ProtocolParams::Generic(params) => {
let username: String = match params.username {
None => String::default(),
Some(u) => format!("{}@", u),
};
format!(
"{}://{}{}:{}",
protocol, username, params.address, params.port
)
}
}
}
}

View File

@@ -125,7 +125,7 @@ impl FileTransferActivity {
Err(err) => match err.kind() {
FileTransferErrorType::UnsupportedFeature => {
// If copy is not supported, perform the tricky copy
self.tricky_copy(entry, dest);
let _ = self.tricky_copy(entry, dest);
}
_ => self.log_and_alert(
LogLevel::Error,
@@ -143,7 +143,7 @@ impl FileTransferActivity {
/// ### tricky_copy
///
/// Tricky copy will be used whenever copy command is not available on remote host
fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) {
pub(super) fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) -> Result<(), String> {
// NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
self.umount_wait();
// match entry
@@ -157,7 +157,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not create temporary file: {}", err),
);
return;
return Err(String::from("Could not create temporary file"));
}
};
// Download file
@@ -170,7 +170,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not download to temporary file: {}", err),
);
return;
return Err(err);
}
// Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
@@ -184,7 +184,7 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err.to_string());
}
};
// Upload file to destination
@@ -202,8 +202,9 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err);
}
Ok(())
}
FsEntry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
@@ -213,7 +214,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not create temporary directory: {}", err),
);
return;
return Err(err.to_string());
}
};
// Get path of dest
@@ -227,7 +228,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: failed to download file: {}", err),
);
return;
return Err(err);
}
// Stat dir
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
@@ -241,7 +242,7 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err.to_string());
}
};
// Upload to destination
@@ -255,8 +256,9 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: failed to send file: {}", err),
);
return;
return Err(err);
}
Ok(())
}
}
}

View File

@@ -27,6 +27,7 @@
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::fs::File;
use std::path::PathBuf;
impl FileTransferActivity {
@@ -99,24 +100,29 @@ impl FileTransferActivity {
};
if let FsEntry::File(local_file) = local_file {
// Create file
match self.client.send_file(&local_file, file_path.as_path()) {
let reader = Box::new(match File::open(tfile.path()) {
Ok(f) => f,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not open tempfile: {}", err),
);
return;
}
});
match self
.client
.send_file_wno_stream(&local_file, file_path.as_path(), reader)
{
Err(err) => self.log_and_alert(
LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err),
),
Ok(writer) => {
// Finalize write
if let Err(err) = self.client.on_sent(writer) {
self.log_and_alert(
LogLevel::Warn,
format!("Could not finalize file: {}", err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
}
Ok(_) => {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
// Reload files
self.reload_remote_dir();
}

View File

@@ -27,6 +27,7 @@
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use crate::filetransfer::FileTransferErrorType;
use std::path::{Path, PathBuf};
impl FileTransferActivity {
@@ -114,6 +115,9 @@ impl FileTransferActivity {
),
);
}
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.tricky_move(entry, dest);
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
@@ -125,4 +129,41 @@ impl FileTransferActivity {
),
}
}
/// ### tricky_move
///
/// Tricky move will be used whenever copy command is not available on remote host.
/// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`)
fn tricky_move(&mut self, entry: &FsEntry, dest: &Path) {
debug!(
"Using tricky-move to move entry {} to {}",
entry.get_abs_path().display(),
dest.display()
);
if self.tricky_copy(entry.clone(), dest).is_ok() {
// Delete remote existing entry
debug!("Tricky-copy worked; removing existing remote entry");
match self.client.remove(entry) {
Ok(_) => self.log(
LogLevel::Info,
format!(
"Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest.display()
),
),
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Copied \"{}\" to \"{}\"; but failed to remove src: {}",
entry.get_abs_path().display(),
dest.display(),
err
),
),
}
} else {
error!("Tricky move aborted due to tricky-copy failure");
}
}
}

View File

@@ -140,6 +140,10 @@ impl ProgressStates {
///
/// Calculate progress in a range between 0.0 to 1.0
pub fn calc_progress(&self) -> f64 {
// Prevent dividing by 0
if self.total == 0 {
return 0.0;
}
let prog: f64 = (self.written as f64) / (self.total as f64);
match prog > 1.0 {
true => 1.0,
@@ -238,6 +242,11 @@ mod test {
// Check if terminated at started
states.started = Instant::now();
assert_eq!(states.calc_bytes_per_second(), 1024);
// Divide by zero
let states: ProgressStates = ProgressStates::default();
assert_eq!(states.total, 0);
assert_eq!(states.written, 0);
assert_eq!(states.calc_progress(), 0.0);
}
#[test]

View File

@@ -23,6 +23,7 @@
*/
// Locals
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
use crate::filetransfer::ProtocolParams;
use crate::system::environment;
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::path;
@@ -134,4 +135,15 @@ impl FileTransferActivity {
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
path::absolutize(self.remote().wrkdir.as_path(), path)
}
/// ### get_remote_hostname
///
/// Get remote hostname
pub(super) fn get_remote_hostname(&self) -> String {
let ft_params = self.context().ft_params().unwrap();
match &ft_params.params {
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
}
}
}

View File

@@ -36,10 +36,8 @@ pub(self) mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
use crate::filetransfer::scp_transfer::ScpFileTransfer;
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::filetransfer::{FileTransfer, FileTransferProtocol, ProtocolParams};
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
use crate::fs::explorer::FileExplorer;
use crate::fs::FsEntry;
use crate::host::Localhost;
@@ -155,6 +153,7 @@ impl FileTransferActivity {
FileTransferProtocol::Scp => {
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
}
FileTransferProtocol::AwsS3 => Box::new(S3FileTransfer::default()),
},
browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
@@ -237,6 +236,28 @@ impl FileTransferActivity {
fn theme(&self) -> &Theme {
self.context().theme_provider().theme()
}
/// ### get_connection_msg
///
/// Get connection message to show to client
fn get_connection_msg(params: &ProtocolParams) -> String {
match params {
ProtocolParams::Generic(params) => {
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.port
);
format!("Connecting to {}:{}", params.address, params.port)
}
ProtocolParams::AwsS3(params) => {
info!(
"Client is not connected to remote; connecting to {} ({})",
params.bucket_name, params.region
);
format!("Connecting to {}", params.bucket_name)
}
}
}
}
/**
@@ -290,12 +311,9 @@ impl Activity for FileTransferActivity {
}
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
let params = self.context().ft_params().unwrap();
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.port
);
let msg: String = format!("Connecting to {}:{}", params.address, params.port);
let ftparams = self.context().ft_params().unwrap();
// print params
let msg: String = Self::get_connection_msg(&ftparams.params);
// Set init state to connecting popup
self.mount_wait(msg.as_str());
// Force ui draw

View File

@@ -34,6 +34,7 @@ use crate::utils::fmt::fmt_millis;
// Ext
use bytesize::ByteSize;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::time::Instant;
@@ -76,22 +77,20 @@ impl FileTransferActivity {
///
/// Connect to remote
pub(super) fn connect(&mut self) {
let params = self.context().ft_params().unwrap().clone();
let addr: String = params.address.clone();
let entry_dir: Option<PathBuf> = params.entry_directory.clone();
let ft_params = self.context().ft_params().unwrap().clone();
let entry_dir: Option<PathBuf> = ft_params.entry_directory.clone();
// Connect to remote
match self.client.connect(
params.address,
params.port,
params.username,
params.password,
) {
match self.client.connect(&ft_params.params) {
Ok(welcome) => {
if let Some(banner) = welcome {
// Log welcome
self.log(
LogLevel::Info,
format!("Established connection with '{}': \"{}\"", addr, banner),
format!(
"Established connection with '{}': \"{}\"",
self.get_remote_hostname(),
banner
),
);
}
// Try to change directory to entry directory
@@ -121,8 +120,7 @@ impl FileTransferActivity {
///
/// disconnect from remote
pub(super) fn disconnect(&mut self) {
let params = self.context().ft_params().unwrap();
let msg: String = format!("Disconnecting from {}", params.address);
let msg: String = format!("Disconnecting from {}", self.get_remote_hostname());
// Show popup disconnecting
self.mount_wait(msg.as_str());
// Disconnect
@@ -442,103 +440,165 @@ impl FileTransferActivity {
// Upload file
// Try to open local file
match self.host.open_file_read(local.abs_path.as_path()) {
Ok(mut fhnd) => match self.client.send_file(local, remote) {
Ok(mut rhnd) => {
// Write file
let file_size: usize =
fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Write remote file
let mut total_bytes_written: usize = 0;
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely written,
// Or filetransfer has been aborted
while total_bytes_written < file_size && !self.transfer.aborted() {
// Handle input events (each 500ms) or if never fetched before
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match fhnd.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match rhnd.write(&buffer[delta..bytes_read]) {
Ok(bytes) => {
delta += bytes;
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(
err,
));
}
}
}
delta
Ok(fhnd) => match self.client.send_file(local, remote) {
Ok(rhnd) => {
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
}
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
}
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
},
Err(err) => Err(TransferErrorReason::HostError(err)),
}
}
/// ### filetransfer_send_one_with_stream
///
/// Send file to remote using stream
fn filetransfer_send_one_with_stream(
&mut self,
local: &FsFile,
remote: &Path,
file_name: String,
mut reader: File,
mut writer: Box<dyn Write>,
) -> Result<(), TransferErrorReason> {
// Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Write remote file
let mut total_bytes_written: usize = 0;
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely written,
// Or filetransfer has been aborted
while total_bytes_written < file_size && !self.transfer.aborted() {
// Handle input events (each 500ms) or if never fetched before
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match reader.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => {
delta += bytes;
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
}
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
};
// Increase progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
delta
}
// Finalize stream
if let Err(err) = self.client.on_sent(rhnd) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// if upload was abrupted, return error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
}
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
},
Err(err) => return Err(TransferErrorReason::HostError(err)),
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
};
// Increase progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_sent(writer) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// if upload was abrupted, return error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
/// ### filetransfer_send_one_wno_stream
///
/// Send an `FsFile` to remote without using streams.
fn filetransfer_send_one_wno_stream(
&mut self,
local: &FsFile,
remote: &Path,
file_name: String,
mut reader: File,
) -> Result<(), TransferErrorReason> {
// Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
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
.send_file_wno_stream(local, remote, Box::new(reader))
{
return Err(TransferErrorReason::FileTransferError(err));
}
// Set transfer size ok
self.transfer.partial.update_progress(file_size);
self.transfer.full.update_progress(file_size);
// Draw again after
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
// log and return Ok
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
@@ -796,120 +856,187 @@ impl FileTransferActivity {
) -> Result<(), TransferErrorReason> {
// Try to open local file
match self.host.open_file_write(local) {
Ok(mut local_file) => {
Ok(local_file) => {
// Download file from remote
match self.client.recv_file(remote) {
Ok(mut rhnd) => {
let mut total_bytes_written: usize = 0;
// Init transfer
self.transfer.partial.init(remote.size);
// Write local file
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read,
// Or filetransfer has been aborted
while total_bytes_written < remote.size && !self.transfer.aborted() {
// Handle input events (each 500 ms) or is None
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match rhnd.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match local_file.write(&buffer[delta..bytes_read]) {
Ok(bytes) => delta += bytes,
Err(err) => {
return Err(TransferErrorReason::LocalIoError(
err,
));
}
}
}
delta
}
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
};
// Set progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_recv(rhnd) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// If download was abrupted, return Error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
// Apply file mode to file
#[cfg(any(
target_family = "unix",
target_os = "macos",
target_os = "linux"
))]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(rhnd) => self.filetransfer_recv_one_with_stream(
local, remote, file_name, rhnd, local_file,
),
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.filetransfer_recv_one_wno_stream(local, remote, file_name)
}
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
}
}
Err(err) => return Err(TransferErrorReason::HostError(err)),
Err(err) => Err(TransferErrorReason::HostError(err)),
}
}
/// ### filetransfer_recv_one_with_stream
///
/// Receive an `FsEntry` from remote using stream
fn filetransfer_recv_one_with_stream(
&mut self,
local: &Path,
remote: &FsFile,
file_name: String,
mut reader: Box<dyn Read>,
mut writer: File,
) -> Result<(), TransferErrorReason> {
let mut total_bytes_written: usize = 0;
// Init transfer
self.transfer.partial.init(remote.size);
// Write local file
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read,
// Or filetransfer has been aborted
while total_bytes_written < remote.size && !self.transfer.aborted() {
// Handle input events (each 500 ms) or is None
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match reader.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => delta += bytes,
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
}
}
delta
}
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
};
// Set progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_recv(reader) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// If download was abrupted, return Error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
// Apply file mode to file
#[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
/// ### filetransfer_recv_one_with_stream
///
/// Receive an `FsEntry` from remote without using stream
fn filetransfer_recv_one_wno_stream(
&mut self,
local: &Path,
remote: &FsFile,
file_name: String,
) -> Result<(), TransferErrorReason> {
// Init transfer
self.transfer.partial.init(remote.size);
// Draw before transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
// recv wno stream
if let Err(err) = self.client.recv_file_wno_stream(remote, local) {
return Err(TransferErrorReason::FileTransferError(err));
}
// Update progress at the end
self.transfer.partial.update_progress(remote.size);
self.transfer.full.update_progress(remote.size);
// Draw after transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
// Apply file mode to file
#[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}

View File

@@ -810,14 +810,14 @@ impl FileTransferActivity {
.store()
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let params = self.context().ft_params().unwrap();
let hostname = self.get_remote_hostname();
let hostname: String = format!(
"{}:{} ",
params.address,
hostname,
fmt_path_elide_ex(
self.remote().wrkdir.as_path(),
width,
params.address.len() + 3 // 3 because of '/…/'
hostname.len() + 3 // 3 because of '/…/'
)
);
let files: Vec<String> = self

View File

@@ -81,12 +81,7 @@ impl SetupActivity {
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_title("Default file transfer protocol", Alignment::Left)
.with_options(&[
String::from("SFTP"),
String::from("SCP"),
String::from("FTP"),
String::from("FTPS"),
])
.with_options(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
.rewind(true)
.build(),
)),
@@ -265,6 +260,7 @@ impl SetupActivity {
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
};
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
let _ = self
@@ -334,6 +330,7 @@ impl SetupActivity {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
};
self.config_mut().set_default_protocol(protocol);