//! ## 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; // -- States and progress /// ### 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 } /// ### full_size /// /// Returns the size of the entire transfer pub fn full_size(&self) -> usize { self.full.total } } 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 { // 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, 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, } } } // -- Options /// ## TransferOpts /// /// Defines the transfer options for transfer actions pub struct TransferOpts { /// Save file as pub save_as: Option, /// Whether to check if file is being replaced pub check_replace: bool, } impl Default for TransferOpts { fn default() -> Self { Self { save_as: None, check_replace: true, } } } impl TransferOpts { /// ### save_as /// /// Define the name of the file to be saved pub fn save_as>(mut self, n: Option) -> Self { self.save_as = n.map(|x| x.as_ref().to_string()); self } /// ### check_replace /// /// Set whether to check if the file being transferred will "replace" an existing one pub fn check_replace(mut self, opt: bool) -> Self { self.check_replace = opt; self } } #[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); // 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] 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); states.full.total = 1024; assert_eq!(states.full_size(), 1024); } #[test] fn transfer_opts() { let opts = TransferOpts::default(); assert_eq!(opts.check_replace, true); assert!(opts.save_as.is_none()); let opts = TransferOpts::default() .check_replace(false) .save_as(Some("omar.txt")); assert_eq!(opts.save_as.as_deref().unwrap(), "omar.txt"); assert_eq!(opts.check_replace, false); } }