remozipsy 0.2.0

Remote Zip Sync - sync remote zip to local fs
Documentation
use core::time::Duration;
use std::{fmt::Debug, time::Instant};

use super::Error;

#[derive(Debug)]
pub enum Progress<RE: Debug, FE: Debug> {
    Incomplete {
        download: ProgressDetails,
        unzip:    ProgressDetails,
        delete:   ProgressDetails,
    },
    Successful,
    Errored(Error<RE, FE>),
}

/// Indicates progress based on bytes, e.g. for filesystem or remote operation
#[derive(Debug, Clone)]
pub struct ProgressDetails {
    total_bytes: u64,
    processed_bytes: u64,
    last_rate_check: Instant,
    processed_since_last_check: u64,
    bytes_per_sec: u64,
}

impl<RE: Debug, FE: Debug> Clone for Progress<RE, FE>
where
    Error<RE, FE>: Clone,
{
    fn clone(&self) -> Self {
        match self {
            Self::Incomplete {
                download,
                unzip,
                delete,
            } => Self::Incomplete {
                download: download.clone(),
                unzip:    unzip.clone(),
                delete:   delete.clone(),
            },
            Self::Successful => Self::Successful,
            Self::Errored(e) => Self::Errored(e.clone()),
        }
    }
}

impl ProgressDetails {
    pub(crate) fn new(total_bytes: u64) -> Self {
        Self {
            total_bytes,
            processed_bytes: 0,
            last_rate_check: Instant::now(),
            processed_since_last_check: 0,
            bytes_per_sec: 0,
        }
    }

    pub(crate) fn add_chunk(&mut self, data: u64) {
        self.processed_bytes += data;
        self.processed_since_last_check += data;

        if self.processed_bytes > self.total_bytes {
            let process = &self;
            tracing::warn!(
                ?process,
                "Processed Bytes is larger than Total Bytes, something seems off"
            );
        }

        let current_time = Instant::now();
        let since_last_check = current_time - self.last_rate_check;
        let since_last_check_f32 = since_last_check.as_secs_f32();
        if since_last_check >= Duration::from_millis(500) || (since_last_check_f32 > 0.0 && self.bytes_per_sec == 0) {
            let bytes_per_sec = (self.processed_since_last_check as f32 / since_last_check_f32) as u64;
            self.processed_since_last_check = 0;
            self.last_rate_check = current_time;
            if self.bytes_per_sec == 0 {
                self.bytes_per_sec = bytes_per_sec;
            } else {
                self.bytes_per_sec = (self.bytes_per_sec * 3 + bytes_per_sec) / 4;
            }
        }
    }

    pub fn total_bytes(&self) -> u64 { self.total_bytes }

    pub fn processed_bytes(&self) -> u64 { self.processed_bytes }

    pub fn is_finished(&self) -> bool { self.processed_bytes >= self.total_bytes }

    pub fn bytes_per_sec(&self) -> u64 { self.bytes_per_sec }

    pub fn percent_complete(&self) -> u64 {
        (self.processed_bytes * 100)
            .checked_div(self.total_bytes)
            .unwrap_or(100)
    }

    pub fn time_remaining(&self) -> Duration {
        Duration::from_secs_f32(
            (self.total_bytes.saturating_sub(self.processed_bytes)) as f32 / self.bytes_per_sec.max(1) as f32,
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add_chunk() {
        let mut details = ProgressDetails::new(1024);
        assert_eq!(details.total_bytes, 1024);
        assert_eq!(details.processed_bytes, 0);

        const QUARTER_TIME: Duration = Duration::from_millis(100);
        // Sleep for 1 second to let rate calculation update
        std::thread::sleep(QUARTER_TIME);

        // Add a chunk of 256 bytes
        details.add_chunk(256);
        assert_eq!(details.processed_bytes, 256);
        assert_ne!(details.bytes_per_sec, 0);
        assert!(!details.is_finished());

        let time_remaining = details.time_remaining();
        assert!(time_remaining > QUARTER_TIME * 2);
        assert!(time_remaining < QUARTER_TIME * 4);

        // Add another chunk of 768 bytes
        details.add_chunk(768);
        assert_eq!(details.processed_bytes, 1024);
        assert_ne!(details.bytes_per_sec, 0);
        assert!(details.is_finished());
    }

    #[test]
    fn test_progress_clone_variants() {
        let progress1 = Progress::<(), ()>::Incomplete {
            download: ProgressDetails::new(0),
            unzip:    ProgressDetails::new(0),
            delete:   ProgressDetails::new(0),
        };
        let progress2 = Progress::<(), ()>::Successful;
        let progress3 = Progress::<(), ()>::Errored(Error::JoinError);

        assert!(matches!(progress1.clone(), Progress::Incomplete { .. }));
        assert!(matches!(progress2.clone(), Progress::Successful));
        assert!(matches!(
            progress3.clone(),
            Progress::<(), ()>::Errored(Error::JoinError)
        ));
    }

    #[test]
    fn test_progress_details() {
        let mut progress1 = ProgressDetails::new(1000);
        assert_eq!(progress1.total_bytes(), 1000);
        assert_eq!(progress1.processed_bytes(), 0);
        assert!(!progress1.is_finished());
        assert_eq!(progress1.percent_complete(), 0);

        progress1.add_chunk(500);
        assert_eq!(progress1.processed_bytes(), 500);
        assert_eq!(progress1.percent_complete(), 50);

        progress1.add_chunk(500);
        assert_eq!(progress1.processed_bytes(), 1000);
        assert_eq!(progress1.percent_complete(), 100);
        assert!(progress1.is_finished());

        let progress2 = ProgressDetails::new(0);
        assert_eq!(progress2.percent_complete(), 100);
        assert!(progress2.is_finished());
    }
}