Refactored transfer states

This commit is contained in:
veeso
2021-05-20 22:44:17 +02:00
parent 6cd9657446
commit e874550d29
6 changed files with 340 additions and 142 deletions

View File

@@ -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<FileExplorer>, // 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;
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -161,6 +161,8 @@ impl FileTransferActivity {
curr_remote_path: &Path,
dst_name: Option<String>,
) {
// 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<String>,
) {
// 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()),
),
);
}

View File

@@ -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)
}