mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Aws s3 support
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user