From e874550d290288cc864f427807537b38a99d9dc7 Mon Sep 17 00:00:00 2001 From: veeso Date: Thu, 20 May 2021 22:44:17 +0200 Subject: [PATCH 1/3] Refactored transfer states --- .../filetransfer/{ => lib}/browser.rs | 10 +- src/ui/activities/filetransfer/lib/mod.rs | 29 ++ .../activities/filetransfer/lib/transfer.rs | 259 ++++++++++++++++++ src/ui/activities/filetransfer/mod.rs | 82 +----- src/ui/activities/filetransfer/session.rs | 78 +++--- src/ui/activities/filetransfer/update.rs | 24 +- 6 files changed, 340 insertions(+), 142 deletions(-) rename src/ui/activities/filetransfer/{ => lib}/browser.rs (95%) create mode 100644 src/ui/activities/filetransfer/lib/mod.rs create mode 100644 src/ui/activities/filetransfer/lib/transfer.rs diff --git a/src/ui/activities/filetransfer/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs similarity index 95% rename from src/ui/activities/filetransfer/browser.rs rename to src/ui/activities/filetransfer/lib/browser.rs index df64876..5fe8147 100644 --- a/src/ui/activities/filetransfer/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -33,7 +33,7 @@ use crate::system::config_client::ConfigClient; /// /// File explorer tab #[derive(Clone, Copy)] -pub(super) enum FileExplorerTab { +pub enum FileExplorerTab { Local, Remote, FindLocal, // Find result tab @@ -43,7 +43,7 @@ pub(super) enum FileExplorerTab { /// ## Browser /// /// Browser contains the browser options -pub(super) struct Browser { +pub struct Browser { local: FileExplorer, // Local File explorer state remote: FileExplorer, // Remote File explorer state found: Option, // File explorer for find result @@ -55,7 +55,7 @@ impl Browser { /// ### new /// /// Build a new `Browser` struct - pub(super) fn new(cli: Option<&ConfigClient>) -> Self { + pub fn new(cli: Option<&ConfigClient>) -> Self { Self { local: Self::build_local_explorer(cli), remote: Self::build_remote_explorer(cli), @@ -99,14 +99,14 @@ impl Browser { self.found = None; } - pub(super) fn tab(&self) -> FileExplorerTab { + pub fn tab(&self) -> FileExplorerTab { self.tab } /// ### change_tab /// /// Update tab value - pub(super) fn change_tab(&mut self, tab: FileExplorerTab) { + pub fn change_tab(&mut self, tab: FileExplorerTab) { self.tab = tab; } diff --git a/src/ui/activities/filetransfer/lib/mod.rs b/src/ui/activities/filetransfer/lib/mod.rs new file mode 100644 index 0000000..4864265 --- /dev/null +++ b/src/ui/activities/filetransfer/lib/mod.rs @@ -0,0 +1,29 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +pub(crate) mod browser; +pub(crate) mod transfer; diff --git a/src/ui/activities/filetransfer/lib/transfer.rs b/src/ui/activities/filetransfer/lib/transfer.rs new file mode 100644 index 0000000..2fc7429 --- /dev/null +++ b/src/ui/activities/filetransfer/lib/transfer.rs @@ -0,0 +1,259 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/** + * MIT License + * + * termscp - Copyright (c) 2021 Christian Visintin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +use bytesize::ByteSize; +use std::fmt; +use std::time::Instant; + +/// ### TransferStates +/// +/// TransferStates contains the states related to the transfer process +pub struct TransferStates { + aborted: bool, // Describes whether the transfer process has been aborted + pub full: ProgressStates, // full transfer states + pub partial: ProgressStates, // Partial transfer states +} + +/// ### ProgressStates +/// +/// Progress states describes the states for the progress of a single transfer part +pub struct ProgressStates { + started: Instant, + total: usize, + written: usize, +} + +impl Default for TransferStates { + fn default() -> Self { + Self::new() + } +} + +impl TransferStates { + /// ### new + /// + /// Instantiates a new transfer states + pub fn new() -> TransferStates { + TransferStates { + aborted: false, + full: ProgressStates::default(), + partial: ProgressStates::default(), + } + } + + /// ### reset + /// + /// Re-intiialize transfer states + pub fn reset(&mut self) { + self.aborted = false; + } + + /// ### abort + /// + /// Set aborted to true + pub fn abort(&mut self) { + self.aborted = true; + } + + /// ### aborted + /// + /// Returns whether transfer has been aborted + pub fn aborted(&self) -> bool { + self.aborted + } +} + +impl Default for ProgressStates { + fn default() -> Self { + ProgressStates { + started: Instant::now(), + written: 0, + total: 0, + } + } +} + +impl fmt::Display for ProgressStates { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let eta: String = match self.calc_eta() { + 0 => String::from("--:--"), + seconds => format!( + "{:0width$}:{:0width$}", + (seconds / 60), + (seconds % 60), + width = 2 + ), + }; + write!( + f, + "{:.2}% - ETA {} ({}/s)", + self.calc_progress_percentage(), + eta, + ByteSize(self.calc_bytes_per_second()) + ) + } +} + +impl ProgressStates { + /// ### init + /// + /// Initialize a new Progress State + pub fn init(&mut self, sz: usize) { + self.started = Instant::now(); + self.total = sz; + self.written = 0; + } + + /// ### update_progress + /// + /// Update progress state + pub fn update_progress(&mut self, delta: usize) -> f64 { + self.written += delta; + self.calc_progress_percentage() + } + + /// ### calc_progress + /// + /// Calculate progress in a range between 0.0 to 1.0 + pub fn calc_progress(&self) -> f64 { + let prog: f64 = (self.written as f64) / (self.total as f64); + match prog > 1.0 { + true => 1.0, + false => prog, + } + } + + /// ### started + /// + /// Get started + pub fn started(&self) -> Instant { + self.started + } + + /// ### calc_progress_percentage + /// + /// Calculate the current transfer progress as percentage + fn calc_progress_percentage(&self) -> f64 { + self.calc_progress() * 100.0 + } + + /// ### calc_bytes_per_second + /// + /// Generic function to calculate bytes per second using elapsed time since transfer started and the bytes written + /// and the total amount of bytes to write + pub fn calc_bytes_per_second(&self) -> u64 { + // bytes_written : elapsed_secs = x : 1 + let elapsed_secs: u64 = self.started.elapsed().as_secs(); + match elapsed_secs { + 0 => match self.written == self.total { + // NOTE: would divide by 0 :D + true => self.total as u64, // Download completed in less than 1 second + false => 0, // 0 B/S + }, + _ => self.written as u64 / elapsed_secs, + } + } + + /// ### calc_eta + /// + /// Calculate ETA for current transfer as seconds + fn calc_eta(&self) -> u64 { + let elapsed_secs: u64 = self.started.elapsed().as_secs(); + let prog: f64 = self.calc_progress_percentage(); + match prog as u64 { + 0 => 0, + _ => ((elapsed_secs * 100) / (prog as u64)) - elapsed_secs, + } + } +} + +#[cfg(test)] +mod test { + + use super::*; + + use pretty_assertions::assert_eq; + use std::time::Duration; + + #[test] + fn test_ui_activities_filetransfer_lib_transfer_progress_states() { + let mut states: ProgressStates = ProgressStates::default(); + assert_eq!(states.total, 0); + assert_eq!(states.written, 0); + assert!(states.started().elapsed().as_secs() < 5); + // Init new transfer + states.init(1024); + assert_eq!(states.total, 1024); + assert_eq!(states.written, 0); + assert_eq!(states.calc_bytes_per_second(), 0); + assert_eq!(states.calc_eta(), 0); + assert_eq!(states.calc_progress_percentage(), 0.0); + assert_eq!(states.calc_progress(), 0.0); + assert_eq!(states.to_string().as_str(), "0.00% - ETA --:-- (0 B/s)"); + // Wait 4 second (virtually) + states.started = states.started.checked_sub(Duration::from_secs(4)).unwrap(); + // Update state + states.update_progress(256); + assert_eq!(states.total, 1024); + assert_eq!(states.written, 256); + assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds + assert_eq!(states.calc_eta(), 12); // 16 total sub 4 + assert_eq!(states.calc_progress_percentage(), 25.0); + assert_eq!(states.calc_progress(), 0.25); + assert_eq!(states.to_string().as_str(), "25.00% - ETA 00:12 (64 B/s)"); + // 100% + states.started = states.started.checked_sub(Duration::from_secs(12)).unwrap(); + states.update_progress(768); + assert_eq!(states.total, 1024); + assert_eq!(states.written, 1024); + assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds + assert_eq!(states.calc_eta(), 0); // 16 total sub 4 + assert_eq!(states.calc_progress_percentage(), 100.0); + assert_eq!(states.calc_progress(), 1.0); + assert_eq!(states.to_string().as_str(), "100.00% - ETA --:-- (64 B/s)"); + // Check if terminated at started + states.started = Instant::now(); + assert_eq!(states.calc_bytes_per_second(), 1024); + } + + #[test] + fn test_ui_activities_filetransfer_lib_transfer_states() { + let mut states: TransferStates = TransferStates::default(); + assert_eq!(states.aborted, false); + assert_eq!(states.full.total, 0); + assert_eq!(states.full.written, 0); + assert!(states.full.started.elapsed().as_secs() < 5); + assert_eq!(states.partial.total, 0); + assert_eq!(states.partial.written, 0); + assert!(states.partial.started.elapsed().as_secs() < 5); + // Aborted + states.abort(); + assert_eq!(states.aborted(), true); + states.reset(); + assert_eq!(states.aborted(), false); + } +} diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 5327ba7..8ebdccc 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -27,7 +27,7 @@ */ // This module is split into files, cause it's just too big pub(self) mod actions; -pub(self) mod browser; +pub(self) mod lib; pub(self) mod misc; pub(self) mod session; pub(self) mod update; @@ -49,14 +49,15 @@ use crate::fs::explorer::FileExplorer; use crate::fs::FsEntry; use crate::host::Localhost; use crate::system::config_client::ConfigClient; -use browser::Browser; +pub(crate) use lib::browser; +use lib::browser::Browser; +use lib::transfer::TransferStates; // Includes use chrono::{DateTime, Local}; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::collections::VecDeque; use std::path::PathBuf; -use std::time::Instant; use tuirealm::View; // -- Storage keys @@ -120,81 +121,6 @@ impl LogRecord { } } -/// ### TransferStates -/// -/// TransferStates contains the states related to the transfer process -struct TransferStates { - pub progress: f64, // Current read/write progress (percentage) - pub started: Instant, // Instant the transfer process started - pub aborted: bool, // Describes whether the transfer process has been aborted - pub bytes_written: usize, // Bytes written during transfer - pub bytes_total: usize, // Total bytes to write -} - -impl TransferStates { - /// ### new - /// - /// Instantiates a new transfer states - pub fn new() -> TransferStates { - TransferStates { - progress: 0.0, - started: Instant::now(), - aborted: false, - bytes_written: 0, - bytes_total: 0, - } - } - - /// ### reset - /// - /// Re-intiialize transfer states - pub fn reset(&mut self) { - self.progress = 0.0; - self.started = Instant::now(); - self.aborted = false; - self.bytes_written = 0; - self.bytes_total = 0; - } - - /// ### set_progress - /// - /// Calculate progress percentage based on current progress - pub fn set_progress(&mut self, w: usize, sz: usize) { - self.bytes_written = w; - self.bytes_total = sz; - let mut prog: f64 = ((self.bytes_written as f64) * 100.0) / (self.bytes_total as f64); - // Check value - if prog > 100.0 { - prog = 100.0; - } else if prog < 0.0 { - prog = 0.0; - } - self.progress = prog; - } - - /// ### byte_per_second - /// - /// Calculate bytes per second - pub fn bytes_per_second(&self) -> u64 { - // bytes_written : elapsed_secs = x : 1 - let elapsed_secs: u64 = self.started.elapsed().as_secs(); - match elapsed_secs { - 0 => match self.bytes_written == self.bytes_total { - // NOTE: would divide by 0 :D - true => self.bytes_total as u64, // Download completed in less than 1 second - false => 0, // 0 B/S - }, - _ => self.bytes_written as u64 / elapsed_secs, - } - } -} - -impl Default for TransferStates { - fn default() -> Self { - Self::new() - } -} - /// ## FileTransferActivity /// /// FileTransferActivity is the data holder for the file transfer activity diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 5257531..38a5804 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -161,6 +161,8 @@ impl FileTransferActivity { curr_remote_path: &Path, dst_name: Option, ) { + // Reset states + self.transfer.reset(); // Write popup let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -229,7 +231,7 @@ impl FileTransferActivity { // Iterate over files for entry in entries.iter() { // If aborted; break - if self.transfer.aborted { + if self.transfer.aborted() { break; } // Send entry; name is always None after first call @@ -264,14 +266,12 @@ impl FileTransferActivity { // Scan dir on remote self.reload_remote_dir(); // If aborted; show popup - if self.transfer.aborted { + if self.transfer.aborted() { // Log abort self.log_and_alert( LogLevel::Warn, format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()), ); - // Set aborted to false - self.transfer.aborted = false; } else { // @! Successful // Eventually, Remove progress bar @@ -290,6 +290,8 @@ impl FileTransferActivity { local_path: &Path, dst_name: Option, ) { + // Reset states + self.transfer.reset(); // Write popup let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -379,7 +381,7 @@ impl FileTransferActivity { // Iterate over files for entry in entries.iter() { // If transfer has been aborted; break - if self.transfer.aborted { + if self.transfer.aborted() { break; } // Receive entry; name is always None after first call @@ -415,7 +417,7 @@ impl FileTransferActivity { // Reload directory on local self.local_scan(local_path); // if aborted; show alert - if self.transfer.aborted { + if self.transfer.aborted() { // Log abort self.log_and_alert( LogLevel::Warn, @@ -424,8 +426,6 @@ impl FileTransferActivity { entry.get_abs_path().display() ), ); - // Reset aborted to false - self.transfer.aborted = false; } else { // Eventually, Reset input mode to explorer self.umount_progress_bar(); @@ -449,21 +449,21 @@ impl FileTransferActivity { // 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; - // Reset transfer states - self.transfer.reset(); let mut last_progress_val: f64 = 0.0; let mut last_input_event_fetch: Instant = Instant::now(); // Mount progress bar self.mount_progress_bar(); // While the entire file hasn't been completely written, // Or filetransfer has been aborted - while total_bytes_written < file_size && !self.transfer.aborted { + while total_bytes_written < file_size && !self.transfer.aborted() { // Handle input events (each 500ms) if last_input_event_fetch.elapsed().as_millis() >= 500 { // Read events @@ -473,18 +473,18 @@ impl FileTransferActivity { } // Read till you can let mut buffer: [u8; 65536] = [0; 65536]; - match fhnd.read(&mut buffer) { + let delta: usize = match fhnd.read(&mut buffer) { Ok(bytes_read) => { total_bytes_written += bytes_read; if bytes_read == 0 { continue; } else { - let mut buf_start: usize = 0; - while buf_start < bytes_read { + let mut delta: usize = 0; + while delta < bytes_read { // Write bytes - match rhnd.write(&buffer[buf_start..bytes_read]) { + match rhnd.write(&buffer[delta..bytes_read]) { Ok(bytes) => { - buf_start += bytes; + delta += bytes; } Err(err) => { self.umount_progress_bar(); @@ -494,21 +494,22 @@ impl FileTransferActivity { } } } + delta } } Err(err) => { self.umount_progress_bar(); return Err(TransferErrorReason::LocalIoError(err)); } - } + }; // Increase progress - self.transfer.set_progress(total_bytes_written, file_size); + self.transfer.partial.update_progress(delta); // Draw only if a significant progress has been made (performance improvement) - if last_progress_val < self.transfer.progress - 1.0 { + 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.progress; + last_progress_val = self.transfer.partial.calc_progress(); } } // Umount progress bar @@ -521,7 +522,7 @@ impl FileTransferActivity { ); } // if upload was abrupted, return error - if self.transfer.aborted { + if self.transfer.aborted() { return Err(TransferErrorReason::Abrupted); } self.log( @@ -530,8 +531,8 @@ impl FileTransferActivity { "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", local.abs_path.display(), remote.display(), - fmt_millis(self.transfer.started.elapsed()), - ByteSize(self.transfer.bytes_per_second()), + fmt_millis(self.transfer.partial.started().elapsed()), + ByteSize(self.transfer.partial.calc_bytes_per_second()), ), ); } @@ -558,8 +559,8 @@ impl FileTransferActivity { match self.client.recv_file(remote) { Ok(mut rhnd) => { let mut total_bytes_written: usize = 0; - // Reset transfer states - self.transfer.reset(); + // 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: Instant = Instant::now(); @@ -567,7 +568,7 @@ impl FileTransferActivity { self.mount_progress_bar(); // While the entire file hasn't been completely read, // Or filetransfer has been aborted - while total_bytes_written < remote.size && !self.transfer.aborted { + while total_bytes_written < remote.size && !self.transfer.aborted() { // Handle input events (each 500 ms) if last_input_event_fetch.elapsed().as_millis() >= 500 { // Read events @@ -577,17 +578,17 @@ impl FileTransferActivity { } // Read till you can let mut buffer: [u8; 65536] = [0; 65536]; - match rhnd.read(&mut buffer) { + let delta: usize = match rhnd.read(&mut buffer) { Ok(bytes_read) => { total_bytes_written += bytes_read; if bytes_read == 0 { continue; } else { - let mut buf_start: usize = 0; - while buf_start < bytes_read { + let mut delta: usize = 0; + while delta < bytes_read { // Write bytes - match local_file.write(&buffer[buf_start..bytes_read]) { - Ok(bytes) => buf_start += bytes, + match local_file.write(&buffer[delta..bytes_read]) { + Ok(bytes) => delta += bytes, Err(err) => { self.umount_progress_bar(); return Err(TransferErrorReason::LocalIoError( @@ -596,21 +597,22 @@ impl FileTransferActivity { } } } + delta } } Err(err) => { self.umount_progress_bar(); return Err(TransferErrorReason::RemoteIoError(err)); } - } + }; // Set progress - self.transfer.set_progress(total_bytes_written, remote.size); + self.transfer.partial.update_progress(delta); // Draw only if a significant progress has been made (performance improvement) - if last_progress_val < self.transfer.progress - 1.0 { + 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.progress; + last_progress_val = self.transfer.partial.calc_progress(); } } // Umount progress bar @@ -623,7 +625,7 @@ impl FileTransferActivity { ); } // If download was abrupted, return Error - if self.transfer.aborted { + if self.transfer.aborted() { return Err(TransferErrorReason::Abrupted); } // Apply file mode to file @@ -648,8 +650,8 @@ impl FileTransferActivity { "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", remote.abs_path.display(), local.display(), - fmt_millis(self.transfer.started.elapsed()), - ByteSize(self.transfer.bytes_per_second()), + fmt_millis(self.transfer.partial.started().elapsed()), + ByteSize(self.transfer.partial.calc_bytes_per_second()), ), ); } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 4c3f156..c6e4f9e 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -42,7 +42,6 @@ use crate::fs::FsEntry; use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxPropsBuilder}; use crate::ui::keymap::*; // externals -use bytesize::ByteSize; use std::path::{Path, PathBuf}; use tuirealm::{ components::progress_bar::ProgressBarPropsBuilder, @@ -644,7 +643,7 @@ impl FileTransferActivity { // -- progress bar (COMPONENT_PROGRESS_BAR, &MSG_KEY_CTRL_C) => { // Set transfer aborted to True - self.transfer.aborted = true; + self.transfer.abort(); None } // -- fallback @@ -796,26 +795,9 @@ impl FileTransferActivity { pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> { match self.view.get_props(COMPONENT_PROGRESS_BAR) { 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 = ProgressBarPropsBuilder::from(props) - .with_texts(Some(text), label) - .with_progress(self.transfer.progress / 100.0) + .with_texts(Some(text), self.transfer.partial.to_string()) + .with_progress(self.transfer.partial.calc_progress()) .build(); self.view.update(COMPONENT_PROGRESS_BAR, props) } From b1da42b543e9e83ae179c1e80775c83506810af0 Mon Sep 17 00:00:00 2001 From: veeso Date: Fri, 21 May 2021 11:49:44 +0200 Subject: [PATCH 2/3] Double progress bar --- src/ui/activities/filetransfer/mod.rs | 3 +- src/ui/activities/filetransfer/session.rs | 337 ++++++++++++++-------- src/ui/activities/filetransfer/update.rs | 25 +- src/ui/activities/filetransfer/view.rs | 51 +++- 4 files changed, 272 insertions(+), 144 deletions(-) diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 8ebdccc..5cedb60 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -70,7 +70,8 @@ const COMPONENT_EXPLORER_LOCAL: &str = "EXPLORER_LOCAL"; const COMPONENT_EXPLORER_REMOTE: &str = "EXPLORER_REMOTE"; const COMPONENT_EXPLORER_FIND: &str = "EXPLORER_FIND"; const COMPONENT_LOG_BOX: &str = "LOG_BOX"; -const COMPONENT_PROGRESS_BAR: &str = "PROGRESS_BAR"; +const COMPONENT_PROGRESS_BAR_FULL: &str = "PROGRESS_BAR_FULL"; +const COMPONENT_PROGRESS_BAR_PARTIAL: &str = "PROGRESS_BAR_PARTIAL"; const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR"; const COMPONENT_TEXT_FATAL: &str = "TEXT_FATAL"; const COMPONENT_TEXT_HELP: &str = "TEXT_HELP"; diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index 38a5804..d9aa06d 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -163,6 +163,23 @@ impl FileTransferActivity { ) { // Reset states self.transfer.reset(); + // Calculate total size of transfer + let total_transfer_size: usize = self.get_total_transfer_size_local(entry); + self.transfer.full.init(total_transfer_size); + // Mount progress bar + self.mount_progress_bar(format!("Uploading {}...", entry.get_abs_path().display())); + // Send recurse + self.filetransfer_send_recurse(entry, curr_remote_path, dst_name); + // Umount progress bar + self.umount_progress_bar(); + } + + fn filetransfer_send_recurse( + &mut self, + entry: &FsEntry, + curr_remote_path: &Path, + dst_name: Option, + ) { // Write popup let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -235,7 +252,11 @@ impl FileTransferActivity { break; } // Send entry; name is always None after first call - self.filetransfer_send(&entry, remote_path.as_path(), None); + self.filetransfer_send_recurse( + &entry, + remote_path.as_path(), + None, + ); } } Err(err) => { @@ -272,13 +293,115 @@ impl FileTransferActivity { LogLevel::Warn, format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()), ); - } else { - // @! Successful - // Eventually, Remove progress bar - self.umount_progress_bar(); } } + /// ### filetransfer_send_file + /// + /// Send local file and write it to remote path + fn filetransfer_send_file( + &mut self, + local: &FsFile, + remote: &Path, + file_name: String, + ) -> Result<(), TransferErrorReason> { + // 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: Instant = Instant::now(); + // 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) + if last_input_event_fetch.elapsed().as_millis() >= 500 { + // Read events + self.read_input_event(); + // Reset instant + last_input_event_fetch = 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 + } + } + 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(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)), + } + Ok(()) + } + /// ### filetransfer_recv /// /// Recv fs entry from remote. @@ -292,6 +415,23 @@ impl FileTransferActivity { ) { // Reset states self.transfer.reset(); + // Calculate total transfer size + let total_transfer_size: usize = self.get_total_transfer_size_remote(entry); + self.transfer.full.init(total_transfer_size); + // Mount progress bar + self.mount_progress_bar(format!("Downloading {}...", entry.get_abs_path().display())); + // Receive + self.filetransfer_recv_recurse(entry, local_path, dst_name); + // Umount progress bar + self.umount_progress_bar(); + } + + fn filetransfer_recv_recurse( + &mut self, + entry: &FsEntry, + local_path: &Path, + dst_name: Option, + ) { // Write popup let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -386,7 +526,11 @@ impl FileTransferActivity { } // Receive entry; name is always None after first call // Local path becomes local_dir_path - self.filetransfer_recv(&entry, local_dir_path.as_path(), None); + self.filetransfer_recv_recurse( + &entry, + local_dir_path.as_path(), + None, + ); } } Err(err) => { @@ -426,123 +570,9 @@ impl FileTransferActivity { entry.get_abs_path().display() ), ); - } else { - // Eventually, Reset input mode to explorer - self.umount_progress_bar(); } } - /// ### filetransfer_send_file - /// - /// Send local file and write it to remote path - fn filetransfer_send_file( - &mut self, - local: &FsFile, - remote: &Path, - file_name: String, - ) -> Result<(), TransferErrorReason> { - // 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: Instant = Instant::now(); - // Mount progress bar - self.mount_progress_bar(); - // 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) - if last_input_event_fetch.elapsed().as_millis() >= 500 { - // Read events - self.read_input_event(); - // Reset instant - last_input_event_fetch = 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) => { - self.umount_progress_bar(); - return Err(TransferErrorReason::RemoteIoError( - err, - )); - } - } - } - delta - } - } - Err(err) => { - self.umount_progress_bar(); - return Err(TransferErrorReason::LocalIoError(err)); - } - }; - // Increase progress - self.transfer.partial.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(); - } - } - // Umount progress bar - self.umount_progress_bar(); - // 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)), - } - Ok(()) - } - /// ### filetransfer_recv_file /// /// Receive file from remote and write it to local path @@ -564,8 +594,6 @@ impl FileTransferActivity { // Write local file let mut last_progress_val: f64 = 0.0; let mut last_input_event_fetch: Instant = Instant::now(); - // Mount progress bar - self.mount_progress_bar(); // While the entire file hasn't been completely read, // Or filetransfer has been aborted while total_bytes_written < remote.size && !self.transfer.aborted() { @@ -590,7 +618,6 @@ impl FileTransferActivity { match local_file.write(&buffer[delta..bytes_read]) { Ok(bytes) => delta += bytes, Err(err) => { - self.umount_progress_bar(); return Err(TransferErrorReason::LocalIoError( err, )); @@ -601,12 +628,12 @@ impl FileTransferActivity { } } Err(err) => { - self.umount_progress_bar(); 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 @@ -615,8 +642,6 @@ impl FileTransferActivity { last_progress_val = self.transfer.partial.calc_progress(); } } - // Umount progress bar - self.umount_progress_bar(); // Finalize stream if let Err(err) = self.client.on_recv(rhnd) { self.log( @@ -1009,4 +1034,64 @@ impl FileTransferActivity { } } } + + // -- transfer sizes + + /// ### get_total_transfer_size_local + /// + /// Get total size of transfer for localhost + fn get_total_transfer_size_local(&mut self, entry: &FsEntry) -> usize { + match entry { + FsEntry::File(file) => file.size, + FsEntry::Directory(dir) => { + // List dir + match self.host.scan_dir(dir.abs_path.as_path()) { + Ok(files) => files + .iter() + .map(|x| self.get_total_transfer_size_local(x)) + .sum(), + Err(err) => { + self.log( + LogLevel::Error, + format!( + "Could not list directory {}: {}", + dir.abs_path.display(), + err + ), + ); + 0 + } + } + } + } + } + + /// ### get_total_transfer_size_remote + /// + /// Get total size of transfer for remote host + fn get_total_transfer_size_remote(&mut self, entry: &FsEntry) -> usize { + match entry { + FsEntry::File(file) => file.size, + FsEntry::Directory(dir) => { + // List directory + match self.client.list_dir(dir.abs_path.as_path()) { + Ok(files) => files + .iter() + .map(|x| self.get_total_transfer_size_remote(x)) + .sum(), + Err(err) => { + self.log( + LogLevel::Error, + format!( + "Could not list directory {}: {}", + dir.abs_path.display(), + err + ), + ); + 0 + } + } + } + } + } } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index c6e4f9e..ecb9292 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -33,9 +33,10 @@ use super::{ COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, 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, + COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL, + COMPONENT_PROGRESS_BAR_PARTIAL, 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; @@ -641,7 +642,7 @@ impl FileTransferActivity { None } // -- progress bar - (COMPONENT_PROGRESS_BAR, &MSG_KEY_CTRL_C) => { + (COMPONENT_PROGRESS_BAR_PARTIAL, &MSG_KEY_CTRL_C) => { // Set transfer aborted to True self.transfer.abort(); None @@ -792,14 +793,22 @@ impl FileTransferActivity { } } - pub(super) fn update_progress_bar(&mut self, text: String) -> Option<(String, Msg)> { - match self.view.get_props(COMPONENT_PROGRESS_BAR) { + pub(super) fn update_progress_bar(&mut self, filename: String) -> Option<(String, Msg)> { + if let Some(props) = self.view.get_props(COMPONENT_PROGRESS_BAR_FULL) { + let root_name: String = props.texts.title.as_deref().unwrap_or("").to_string(); + let props = ProgressBarPropsBuilder::from(props) + .with_texts(Some(root_name), self.transfer.full.to_string()) + .with_progress(self.transfer.full.calc_progress()) + .build(); + let _ = self.view.update(COMPONENT_PROGRESS_BAR_FULL, props); + } + match self.view.get_props(COMPONENT_PROGRESS_BAR_PARTIAL) { Some(props) => { let props = ProgressBarPropsBuilder::from(props) - .with_texts(Some(text), self.transfer.partial.to_string()) + .with_texts(Some(filename), self.transfer.partial.to_string()) .with_progress(self.transfer.partial.calc_progress()) .build(); - self.view.update(COMPONENT_PROGRESS_BAR, props) + self.view.update(COMPONENT_PROGRESS_BAR_PARTIAL, props) } None => None, } diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index d32c35b..bab6290 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -247,12 +247,25 @@ impl FileTransferActivity { self.view.render(super::COMPONENT_LIST_FILEINFO, f, popup); } } - if let Some(props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR) { + if let Some(props) = self.view.get_props(super::COMPONENT_PROGRESS_BAR_PARTIAL) { if props.visible { - let popup = draw_area_in(f.size(), 40, 10); + let popup = draw_area_in(f.size(), 50, 20); f.render_widget(Clear, popup); // make popup - self.view.render(super::COMPONENT_PROGRESS_BAR, f, popup); + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(50), // Full + Constraint::Percentage(50), // Partial + ] + .as_ref(), + ) + .split(popup); + self.view + .render(super::COMPONENT_PROGRESS_BAR_FULL, f, popup_chunks[0]); + self.view + .render(super::COMPONENT_PROGRESS_BAR_PARTIAL, f, popup_chunks[1]); } } if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DELETE) { @@ -614,23 +627,43 @@ impl FileTransferActivity { self.view.umount(super::COMPONENT_INPUT_SAVEAS); } - pub(super) fn mount_progress_bar(&mut self) { + pub(super) fn mount_progress_bar(&mut self, root_name: String) { self.view.mount( - super::COMPONENT_PROGRESS_BAR, + super::COMPONENT_PROGRESS_BAR_FULL, Box::new(ProgressBar::new( ProgressBarPropsBuilder::default() - .with_progbar_color(Color::LightGreen) + .with_progbar_color(Color::Green) .with_background(Color::Black) - .with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen) + .with_borders( + Borders::TOP | Borders::RIGHT | Borders::LEFT, + BorderType::Rounded, + Color::Reset, + ) + .with_texts(Some(root_name), String::new()) + .build(), + )), + ); + self.view.mount( + super::COMPONENT_PROGRESS_BAR_PARTIAL, + Box::new(ProgressBar::new( + ProgressBarPropsBuilder::default() + .with_progbar_color(Color::Green) + .with_background(Color::Black) + .with_borders( + Borders::BOTTOM | Borders::RIGHT | Borders::LEFT, + BorderType::Rounded, + Color::Reset, + ) .with_texts(Some(String::from("Please wait")), String::new()) .build(), )), ); - self.view.active(super::COMPONENT_PROGRESS_BAR); + self.view.active(super::COMPONENT_PROGRESS_BAR_PARTIAL); } pub(super) fn umount_progress_bar(&mut self) { - self.view.umount(super::COMPONENT_PROGRESS_BAR); + self.view.umount(super::COMPONENT_PROGRESS_BAR_PARTIAL); + self.view.umount(super::COMPONENT_PROGRESS_BAR_FULL); } pub(super) fn mount_file_sorting(&mut self) { From d6d8197869e191cb2d0b1014fa895eb383e16761 Mon Sep 17 00:00:00 2001 From: veeso Date: Fri, 21 May 2021 12:08:53 +0200 Subject: [PATCH 3/3] feature changelog --- CHANGELOG.md | 7 +++++++ README.md | 14 +++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e69da8..44e5b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ Released on FIXME: ?? +๐ŸŒธ The Spring Update ๐ŸŒท + - **Synchronized browsing**: - Added the possibility to enabled the synchronized brower navigation - when you enter a directory, the same directory will be entered on the other tab @@ -41,6 +43,11 @@ Released on FIXME: ?? - Added **COPY** command to SFTP (Please note that Copy command is not supported by SFTP natively, so here it just uses the `cp` shell command as it does in SCP). - *FTP* - Added support for file copy (achieved through *tricky-copy*: the file is first downloaded, then uploaded with a different file name) +- **Double progress bar**: + - From now one two progress bar will be displayed: + - the first, on top, displays the full transfer state (e.g. when downloading a directory of 10 files, the progress of the entire transfer) + - the second, on bottom, displays the transfer of the individual file being written (as happened for the old versions) + - changed the progress bar colour from `LightGreen` to `Green` - Enhancements - Added a status bar in the file explorer showing whether the sync browser is enabled and which file sorting mode is selected - Removed the goold old figlet title diff --git a/README.md b/README.md index 9ab1b29..5dc08d8 100644 --- a/README.md +++ b/README.md @@ -182,15 +182,15 @@ The developer documentation can be found on Rust Docs at