Files
termscp/src/ui/activities/filetransfer_activity/update.rs
2021-03-20 21:06:12 +01:00

722 lines
31 KiB
Rust

//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
/*
*
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
// deps
extern crate bytesize;
// locals
use super::{
FileExplorerTab, FileTransferActivity, LogLevel, COMPONENT_EXPLORER_LOCAL,
COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR,
COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR, COMPONENT_RADIO_DELETE,
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
};
use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::ui::activities::keymap::*;
use crate::ui::layout::props::{TableBuilder, TextParts, TextSpan, TextSpanBuilder};
use crate::ui::layout::{Msg, Payload};
// externals
use bytesize::ByteSize;
use std::path::{Path, PathBuf};
use tui::style::Color;
impl FileTransferActivity {
// -- update
/// ### update
///
/// Update auth activity model based on msg
/// The function exits when returns None
pub(super) fn update(&mut self, msg: Option<(String, Msg)>) -> Option<(String, Msg)> {
let ref_msg: Option<(&str, &Msg)> = match msg.as_ref() {
None => None,
Some((s, msg)) => Some((s, msg)),
};
// Match msg
match ref_msg {
None => None, // Exit after None
Some(msg) => match msg {
// -- local tab
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_RIGHT) => {
// Change tab
self.view.active(COMPONENT_EXPLORER_REMOTE);
self.tab = FileExplorerTab::Remote;
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_BACKSPACE) => {
// Go to previous directory
if let Some(d) = self.local.popd() {
self.local_changedir(d.as_path(), false);
}
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => {
// Toggle hidden files
self.local.toggle_hidden_files();
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => {
let file: Option<FsEntry> = match self.get_local_file_entry() {
Some(f) => Some(f.clone()),
None => None,
};
if let Some(file) = file {
self.mount_file_info(&file);
}
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_L) => {
// Reload directory
let pwd: PathBuf = self.local.wrkdir.clone();
self.local_scan(pwd.as_path());
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_O) => {
// Clone entry due to mutable stuff...
if self.get_local_file_entry().is_some() {
let fsentry: FsEntry = self.get_local_file_entry().unwrap().clone();
// Check if file
if fsentry.is_file() {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"...", fsentry.get_abs_path().display())
.as_str(),
);
// Edit file
match self.edit_local_file(fsentry.get_abs_path().as_path()) {
Ok(_) => {
// Reload directory
let pwd: PathBuf = self.local.wrkdir.clone();
self.local_scan(pwd.as_path());
}
Err(err) => self.log_and_alert(LogLevel::Error, err),
}
}
}
// Reload file list component
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_U) => {
// Get pwd
let path: PathBuf = self.local.wrkdir.clone();
// Go to parent directory
if let Some(parent) = path.as_path().parent() {
self.local_changedir(parent, true);
// Reload file list component
}
self.update_local_filelist()
}
// -- remote tab
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_LEFT) => {
// Change tab
self.view.active(COMPONENT_EXPLORER_LOCAL);
self.tab = FileExplorerTab::Local;
None
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => {
// Go to previous directory
if let Some(d) = self.remote.popd() {
self.remote_changedir(d.as_path(), false);
}
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_A) => {
// Toggle hidden files
self.remote.toggle_hidden_files();
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => {
let file: Option<FsEntry> = match self.get_remote_file_entry() {
Some(f) => Some(f.clone()),
None => None,
};
if let Some(file) = file {
self.mount_file_info(&file);
}
None
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_L) => {
// Reload directory
let pwd: PathBuf = self.remote.wrkdir.clone();
self.remote_scan(pwd.as_path());
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_O) => {
// Clone entry due to mutable stuff...
if self.get_remote_file_entry().is_some() {
let fsentry: FsEntry = self.get_remote_file_entry().unwrap().clone();
// Check if file
if let FsEntry::File(file) = fsentry.clone() {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"...", fsentry.get_abs_path().display())
.as_str(),
);
// Edit file
match self.edit_remote_file(&file) {
Ok(_) => {
// Reload directory
let pwd: PathBuf = self.remote.wrkdir.clone();
self.remote_scan(pwd.as_path());
}
Err(err) => self.log_and_alert(LogLevel::Error, err),
}
}
}
// Reload file list component
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_U) => {
// Get pwd
let path: PathBuf = self.remote.wrkdir.clone();
// Go to parent directory
if let Some(parent) = path.as_path().parent() {
self.remote_changedir(parent, true);
}
// Reload file list component
self.update_remote_filelist()
}
// -- common explorer keys
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_B)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_B) => {
// Show sorting file
self.mount_file_sorting();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_C)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_C) => {
self.mount_copy();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_D)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_D) => {
self.mount_mkdir();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_G)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_G) => {
self.mount_goto();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_H)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_H) => {
self.mount_help();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_N)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_N) => {
self.mount_newfile();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_Q)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_Q)
| (COMPONENT_LOG_BOX, &MSG_KEY_CHAR_Q) => {
self.mount_quit();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_R)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_R) => {
// Mount rename
self.mount_rename();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_S)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_S) => {
// Mount rename
self.mount_saveas();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_ESC)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_ESC)
| (COMPONENT_LOG_BOX, &MSG_KEY_ESC) => {
self.mount_disconnect();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_DEL)
| (COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_E)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_DEL)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_E) => {
self.mount_radio_delete();
None
}
// -- switch to log
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_TAB)
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_TAB) => {
self.view.active(COMPONENT_LOG_BOX); // Active log box
None
}
// -- Log box
(COMPONENT_LOG_BOX, &MSG_KEY_TAB) => {
self.view.blur(); // Blur log box
None
}
// -- copy popup
(COMPONENT_INPUT_COPY, &MSG_KEY_ESC) => {
self.umount_copy();
None
}
(COMPONENT_INPUT_COPY, Msg::OnSubmit(Payload::Text(input))) => {
// Copy file
match self.tab {
FileExplorerTab::Local => self.action_local_copy(input.to_string()),
FileExplorerTab::Remote => self.action_remote_copy(input.to_string()),
}
self.umount_copy();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
}
}
// -- goto popup
(COMPONENT_INPUT_GOTO, &MSG_KEY_ESC) => {
self.umount_goto();
None
}
(COMPONENT_INPUT_GOTO, Msg::OnSubmit(Payload::Text(input))) => {
match self.tab {
FileExplorerTab::Local => self.action_change_local_dir(input.to_string()),
FileExplorerTab::Remote => self.action_change_remote_dir(input.to_string()),
}
// Umount
self.umount_goto();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
}
}
// -- make directory
(COMPONENT_INPUT_MKDIR, &MSG_KEY_ESC) => {
self.umount_mkdir();
None
}
(COMPONENT_INPUT_MKDIR, Msg::OnSubmit(Payload::Text(input))) => {
match self.tab {
FileExplorerTab::Local => self.action_local_mkdir(input.to_string()),
FileExplorerTab::Remote => self.action_remote_mkdir(input.to_string()),
}
self.umount_mkdir();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
}
}
// -- new file
(COMPONENT_INPUT_NEWFILE, &MSG_KEY_ESC) => {
self.umount_newfile();
None
}
(COMPONENT_INPUT_NEWFILE, Msg::OnSubmit(Payload::Text(input))) => {
match self.tab {
FileExplorerTab::Local => self.action_local_newfile(input.to_string()),
FileExplorerTab::Remote => self.action_remote_newfile(input.to_string()),
}
self.umount_newfile();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
}
}
// -- rename
(COMPONENT_INPUT_RENAME, &MSG_KEY_ESC) => {
self.umount_rename();
None
}
(COMPONENT_INPUT_RENAME, Msg::OnSubmit(Payload::Text(input))) => {
match self.tab {
FileExplorerTab::Local => self.action_local_rename(input.to_string()),
FileExplorerTab::Remote => self.action_remote_rename(input.to_string()),
}
self.umount_rename();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
}
}
// -- save as
(COMPONENT_INPUT_SAVEAS, &MSG_KEY_ESC) => {
self.umount_saveas();
None
}
(COMPONENT_INPUT_SAVEAS, Msg::OnSubmit(Payload::Text(input))) => {
match self.tab {
FileExplorerTab::Local => self.action_local_saveas(input.to_string()),
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
}
self.umount_saveas();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
}
}
// -- fileinfo
(COMPONENT_LIST_FILEINFO, &MSG_KEY_ENTER)
| (COMPONENT_LIST_FILEINFO, &MSG_KEY_ESC) => {
self.umount_file_info();
None
}
// -- delete
(COMPONENT_RADIO_DELETE, &MSG_KEY_ESC)
| (COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::Unsigned(1))) => {
self.umount_radio_delete();
None
}
(COMPONENT_RADIO_DELETE, Msg::OnSubmit(Payload::Unsigned(0))) => {
// Choice is 'YES'
match self.tab {
FileExplorerTab::Local => self.action_local_delete(),
FileExplorerTab::Remote => self.action_remote_delete(),
}
self.umount_radio_delete();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
}
}
// -- disconnect
(COMPONENT_RADIO_DISCONNECT, &MSG_KEY_ESC)
| (COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::Unsigned(1))) => {
self.umount_disconnect();
None
}
(COMPONENT_RADIO_DISCONNECT, Msg::OnSubmit(Payload::Unsigned(0))) => {
self.disconnect();
self.umount_disconnect();
None
}
// -- quit
(COMPONENT_RADIO_QUIT, &MSG_KEY_ESC)
| (COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(1))) => {
self.umount_quit();
None
}
(COMPONENT_RADIO_QUIT, Msg::OnSubmit(Payload::Unsigned(0))) => {
self.disconnect_and_quit();
self.umount_quit();
None
}
(COMPONENT_RADIO_SORTING, &MSG_KEY_ESC) => {
self.umount_file_sorting();
None
}
(COMPONENT_RADIO_SORTING, Msg::OnSubmit(Payload::Unsigned(mode))) => {
// Get sorting mode
let sorting: FileSorting = match mode {
1 => FileSorting::ByModifyTime,
2 => FileSorting::ByCreationTime,
3 => FileSorting::BySize,
_ => FileSorting::ByName,
};
match self.tab {
FileExplorerTab::Local => self.local.sort_by(sorting),
FileExplorerTab::Remote => self.remote.sort_by(sorting),
}
self.umount_file_sorting();
// Reload files
match self.tab {
FileExplorerTab::Local => self.update_local_filelist(),
FileExplorerTab::Remote => self.update_remote_filelist(),
}
}
// -- error
(COMPONENT_TEXT_ERROR, &MSG_KEY_ESC) | (COMPONENT_TEXT_ERROR, &MSG_KEY_ENTER) => {
self.umount_error();
None
}
// -- fatal
(COMPONENT_TEXT_FATAL, &MSG_KEY_ESC) | (COMPONENT_TEXT_FATAL, &MSG_KEY_ENTER) => {
self.disconnected = true;
None
}
// -- help
(COMPONENT_TEXT_HELP, &MSG_KEY_ESC) | (COMPONENT_TEXT_HELP, &MSG_KEY_ENTER) => {
self.umount_help();
None
}
// -- fallback
(_, _) => None, // Nothing to do
},
}
}
/// ### update_local_filelist
///
/// Update local file list
pub(super) fn update_local_filelist(&mut self) -> Option<(String, Msg)> {
match self
.view
.get_props(super::COMPONENT_EXPLORER_LOCAL)
.as_mut()
{
Some(props) => {
// Get width
let width: usize = match self
.context
.as_ref()
.unwrap()
.store
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
{
Some(val) => val,
None => 256, // Default
};
let hostname: String = match hostname::get() {
Ok(h) => {
let hostname: String = h.as_os_str().to_string_lossy().to_string();
let tokens: Vec<&str> = hostname.split('.').collect();
String::from(*tokens.get(0).unwrap_or(&"localhost"))
}
Err(_) => String::from("localhost"),
};
let hostname: String = format!(
"{}:{} ",
hostname,
FileTransferActivity::elide_wrkdir_path(
self.local.wrkdir.as_path(),
hostname.as_str(),
width
)
.display()
);
let files: Vec<TextSpan> = self
.local
.iter_files()
.map(|x: &FsEntry| TextSpan::from(self.local.fmt_file(x)))
.collect();
// Update
let props = props
.with_texts(TextParts::new(Some(hostname), Some(files)))
.build();
// Update
self.view.update(super::COMPONENT_EXPLORER_LOCAL, props)
}
None => None,
}
}
/// ### update_remote_filelist
///
/// Update remote file list
pub(super) fn update_remote_filelist(&mut self) -> Option<(String, Msg)> {
match self
.view
.get_props(super::COMPONENT_EXPLORER_REMOTE)
.as_mut()
{
Some(props) => {
// Get width
let width: usize = match self
.context
.as_ref()
.unwrap()
.store
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
{
Some(val) => val,
None => 256, // Default
};
let hostname: String = format!(
"{}:{} ",
self.params.address,
FileTransferActivity::elide_wrkdir_path(
self.remote.wrkdir.as_path(),
self.params.address.as_str(),
width
)
.display()
);
let files: Vec<TextSpan> = self
.remote
.iter_files()
.map(|x: &FsEntry| TextSpan::from(self.remote.fmt_file(x)))
.collect();
// Update
let props = props
.with_texts(TextParts::new(Some(hostname), Some(files)))
.build();
self.view.update(super::COMPONENT_EXPLORER_REMOTE, props)
}
None => None,
}
}
/// ### update_logbox
///
/// Update log box
pub(super) fn update_logbox(&mut self) -> Option<(String, Msg)> {
match self.view.get_props(super::COMPONENT_LOG_BOX).as_mut() {
Some(props) => {
// Get width
let width: usize = match self
.context
.as_ref()
.unwrap()
.store
.get_unsigned(super::STORAGE_LOGBOX_WIDTH)
{
Some(val) => val,
None => 256, // Default
};
// Make log entries
let mut table: TableBuilder = TableBuilder::default();
for (idx, record) in self.log_records.iter().enumerate() {
let record_rows = textwrap::wrap(record.msg.as_str(), (width as usize) - 35); // -35 'cause log prefix
// Add row if not first row
if idx > 0 {
table.add_row();
}
let fg = match record.level {
LogLevel::Error => Color::Red,
LogLevel::Warn => Color::Yellow,
LogLevel::Info => Color::Green,
};
for (idx, row) in record_rows.iter().enumerate() {
match idx {
0 => {
// First row
table
.add_col(TextSpan::from(format!(
"{}",
record.time.format("%Y-%m-%dT%H:%M:%S%Z")
)))
.add_col(TextSpan::from(" ["))
.add_col(
TextSpanBuilder::new(
format!(
"{:5}",
match record.level {
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
}
)
.as_str(),
)
.with_foreground(fg)
.build(),
)
.add_col(TextSpan::from("]: "))
.add_col(TextSpan::from(row.as_ref()));
}
_ => {
table.add_col(TextSpan::from(textwrap::indent(
row.as_ref(),
" ",
)));
}
}
}
}
let table = table.build();
let props = props
.with_texts(TextParts::table(Some(String::from("Log")), table))
.build();
self.view.update(super::COMPONENT_LOG_BOX, props)
}
None => None,
}
}
pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> {
match self.view.get_props(COMPONENT_PROGRESS_BAR).as_mut() {
Some(props) => {
// Calculate ETA
let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs();
let eta: String = match self.transfer.progress as u64 {
0 => String::from("--:--"), // NOTE: would divide by 0 :D
_ => {
let eta: u64 =
((elapsed_secs * 100) / (self.transfer.progress as u64)) - elapsed_secs;
format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2)
}
};
// Calculate bytes/s
let label = format!(
"{:.2}% - ETA {} ({}/s)",
self.transfer.progress,
eta,
ByteSize(self.transfer.bytes_per_second())
);
let props = props
.with_texts(TextParts::new(
Some(text),
Some(vec![TextSpan::from(label)]),
))
.build();
self.view.update(COMPONENT_PROGRESS_BAR, props)
}
None => None,
}
}
/// ### elide_wrkdir_path
///
/// Elide working directory path if longer than width + host.len
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
fn elide_wrkdir_path(wrkdir: &Path, host: &str, width: usize) -> PathBuf {
let fmt_path: String = format!("{}", wrkdir.display());
// NOTE: +5 is const
match fmt_path.len() + host.len() + 5 > width {
false => PathBuf::from(wrkdir),
true => {
// Elide
let ancestors_len: usize = wrkdir.ancestors().count();
let mut ancestors = wrkdir.ancestors();
let mut elided_path: PathBuf = PathBuf::new();
// If ancestors_len's size is bigger than 2, push count - 2
if ancestors_len > 2 {
elided_path.push(ancestors.nth(ancestors_len - 2).unwrap());
}
// If ancestors_len is bigger than 3, push '...' and parent too
if ancestors_len > 3 {
elided_path.push("...");
if let Some(parent) = wrkdir.ancestors().nth(1) {
elided_path.push(parent.file_name().unwrap());
}
}
// Push file_name
if let Some(name) = wrkdir.file_name() {
elided_path.push(name);
}
elided_path
}
}
}
}