use std::time::Duration;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DownloadStats {
pub total_files: usize,
pub files_completed: usize,
pub files_failed: usize,
pub files_in_progress: usize,
pub total_bytes_downloaded: u64,
pub download_rate_bps: f64,
pub estimated_completion: Option<DateTime<Utc>>,
pub active_workers: usize,
pub session_start: DateTime<Utc>,
pub session_duration: Duration,
}
impl Default for DownloadStats {
fn default() -> Self {
Self {
total_files: 0,
files_completed: 0,
files_failed: 0,
files_in_progress: 0,
total_bytes_downloaded: 0,
download_rate_bps: 0.0,
estimated_completion: None,
active_workers: 0,
session_start: Utc::now(),
session_duration: Duration::ZERO,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionResult {
pub stats: DownloadStats,
pub success: bool,
pub shutdown_errors: Vec<String>,
pub total_duration: Duration,
}
impl DownloadStats {
pub fn new_with_expected_files(expected_files: usize) -> Self {
Self {
total_files: expected_files,
..Default::default()
}
}
pub fn completion_percentage(&self) -> f64 {
if self.total_files == 0 {
return 0.0;
}
(self.files_completed as f64 / self.total_files as f64) * 100.0
}
pub fn total_processed(&self) -> usize {
self.files_completed + self.files_failed
}
pub fn is_complete(&self) -> bool {
self.total_files > 0 && self.total_processed() >= self.total_files
}
pub fn calculate_eta(&mut self) {
if self.download_rate_bps > 0.0 {
let remaining_files = self.total_files.saturating_sub(self.files_completed);
if remaining_files > 0 {
let avg_file_size = if self.files_completed > 0 {
self.total_bytes_downloaded as f64 / self.files_completed as f64
} else {
1024.0 };
let remaining_bytes = remaining_files as f64 * avg_file_size;
let eta_seconds = remaining_bytes / self.download_rate_bps;
self.estimated_completion =
Some(Utc::now() + chrono::Duration::seconds(eta_seconds as i64));
} else {
self.estimated_completion = Some(Utc::now());
}
}
}
pub fn update_duration(&mut self) {
self.session_duration = Utc::now()
.signed_duration_since(self.session_start)
.to_std()
.unwrap_or(Duration::ZERO);
}
pub fn format_download_rate(&self) -> String {
if self.download_rate_bps < 1024.0 {
format!("{:.1} B/s", self.download_rate_bps)
} else if self.download_rate_bps < 1024.0 * 1024.0 {
format!("{:.1} KB/s", self.download_rate_bps / 1024.0)
} else {
format!("{:.1} MB/s", self.download_rate_bps / (1024.0 * 1024.0))
}
}
pub fn format_eta(&self) -> String {
match self.estimated_completion {
Some(eta) => {
let duration = eta.signed_duration_since(Utc::now());
if let Ok(std_duration) = duration.to_std() {
format_duration(std_duration)
} else {
"Complete".to_string()
}
}
None => "Unknown".to_string(),
}
}
}
impl SessionResult {
pub fn success(stats: DownloadStats, total_duration: Duration) -> Self {
Self {
stats,
success: true,
shutdown_errors: Vec::new(),
total_duration,
}
}
pub fn failed(stats: DownloadStats, total_duration: Duration, errors: Vec<String>) -> Self {
Self {
stats,
success: false,
shutdown_errors: errors,
total_duration,
}
}
pub fn has_errors(&self) -> bool {
!self.shutdown_errors.is_empty()
}
pub fn summary(&self) -> String {
if self.success && !self.has_errors() {
format!(
"Session completed successfully: {} files in {:?}",
self.stats.files_completed, self.total_duration
)
} else if self.success {
format!(
"Session completed with warnings: {} files in {:?}, {} warnings",
self.stats.files_completed,
self.total_duration,
self.shutdown_errors.len()
)
} else {
format!(
"Session failed: {} files in {:?}, {} errors",
self.stats.files_completed,
self.total_duration,
self.shutdown_errors.len()
)
}
}
}
fn format_duration(duration: Duration) -> String {
let total_secs = duration.as_secs();
if total_secs < 60 {
format!("{}s", total_secs)
} else if total_secs < 3600 {
format!("{}m{}s", total_secs / 60, total_secs % 60)
} else {
format!("{}h{}m", total_secs / 3600, (total_secs % 3600) / 60)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stats_calculations() {
let mut stats = DownloadStats::new_with_expected_files(100);
stats.files_completed = 60;
stats.files_failed = 10;
assert_eq!(stats.completion_percentage(), 60.0);
assert_eq!(stats.total_processed(), 70);
assert!(!stats.is_complete());
stats.files_completed = 90;
assert!(stats.is_complete());
}
#[test]
fn test_eta_calculation() {
let mut stats = DownloadStats::new_with_expected_files(100);
stats.files_completed = 50;
stats.total_bytes_downloaded = 50 * 1024; stats.download_rate_bps = 1024.0;
stats.calculate_eta();
assert!(stats.estimated_completion.is_some());
}
#[test]
fn test_download_rate_formatting() {
let mut stats = DownloadStats {
download_rate_bps: 512.0,
..Default::default()
};
assert_eq!(stats.format_download_rate(), "512.0 B/s");
stats.download_rate_bps = 1536.0; assert_eq!(stats.format_download_rate(), "1.5 KB/s");
stats.download_rate_bps = 2.5 * 1024.0 * 1024.0; assert_eq!(stats.format_download_rate(), "2.5 MB/s");
}
#[test]
fn test_duration_formatting() {
assert_eq!(format_duration(Duration::from_secs(30)), "30s");
assert_eq!(format_duration(Duration::from_secs(90)), "1m30s");
assert_eq!(format_duration(Duration::from_secs(3665)), "1h1m");
}
#[test]
fn test_session_result_creation() {
let stats = DownloadStats::default();
let duration = Duration::from_secs(60);
let success_result = SessionResult::success(stats.clone(), duration);
assert!(success_result.success);
assert!(!success_result.has_errors());
let errors = vec!["Test error".to_string()];
let failed_result = SessionResult::failed(stats, duration, errors);
assert!(!failed_result.success);
assert!(failed_result.has_errors());
}
#[test]
fn test_session_summary() {
let stats = DownloadStats::default();
let duration = Duration::from_secs(60);
let success_result = SessionResult::success(stats.clone(), duration);
let summary = success_result.summary();
assert!(summary.contains("completed successfully"));
let errors = vec!["Warning".to_string()];
let warning_result = SessionResult::success(stats.clone(), duration);
let mut warning_result = warning_result;
warning_result.shutdown_errors = errors;
let summary = warning_result.summary();
assert!(summary.contains("warnings") || summary.contains("completed successfully"));
}
}