use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tracing::{debug, info};
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DownloadStatus {
InProgress,
Completed,
Cancelled,
Failed(String),
}
#[derive(Debug, Clone)]
pub struct DownloadInfo {
pub id: String,
pub url: String,
pub filename: String,
pub save_path: Option<PathBuf>,
pub total_size: Option<u64>,
pub downloaded: u64,
pub status: DownloadStatus,
}
#[derive(Debug, Default)]
pub struct DownloadManager {
downloads: Mutex<Vec<DownloadInfo>>,
default_dir: PathBuf,
}
impl DownloadManager {
pub fn new() -> Self {
let default_dir = std::env::temp_dir().join("rpage_downloads");
Self {
downloads: Mutex::new(Vec::new()),
default_dir,
}
}
pub fn with_dir(dir: impl Into<PathBuf>) -> Self {
Self {
downloads: Mutex::new(Vec::new()),
default_dir: dir.into(),
}
}
pub fn default_dir(&self) -> &Path {
&self.default_dir
}
pub fn register(&self, url: &str, filename: &str) -> String {
let id = format!("dl_{}", self.downloads.lock().map(|l| l.len()).unwrap_or(0));
let info = DownloadInfo {
id: id.clone(),
url: url.to_string(),
filename: filename.to_string(),
save_path: None,
total_size: None,
downloaded: 0,
status: DownloadStatus::InProgress,
};
if let Ok(mut list) = self.downloads.lock() {
list.push(info);
}
debug!("Registered download: {id} -> {filename}");
id
}
pub fn update_progress(&self, id: &str, downloaded: u64) {
if let Ok(mut list) = self.downloads.lock() {
if let Some(dl) = list.iter_mut().find(|d| d.id == id) {
dl.downloaded = downloaded;
}
}
}
pub fn complete(&self, id: &str, save_path: &Path) {
if let Ok(mut list) = self.downloads.lock() {
if let Some(dl) = list.iter_mut().find(|d| d.id == id) {
dl.status = DownloadStatus::Completed;
dl.save_path = Some(save_path.to_path_buf());
dl.downloaded = dl.total_size.unwrap_or(dl.downloaded);
info!("Download completed: {id} -> {}", save_path.display());
}
}
}
pub fn fail(&self, id: &str, error: &str) {
if let Ok(mut list) = self.downloads.lock() {
if let Some(dl) = list.iter_mut().find(|d| d.id == id) {
dl.status = DownloadStatus::Failed(error.to_string());
}
}
}
pub fn cancel(&self, id: &str) {
if let Ok(mut list) = self.downloads.lock() {
if let Some(dl) = list.iter_mut().find(|d| d.id == id) {
dl.status = DownloadStatus::Cancelled;
}
}
}
pub fn list(&self) -> Vec<DownloadInfo> {
self.downloads.lock().map(|l| l.clone()).unwrap_or_default()
}
pub fn get(&self, id: &str) -> Option<DownloadInfo> {
self.downloads
.lock()
.ok()
.and_then(|l| l.iter().find(|d| d.id == id).cloned())
}
pub fn completed(&self) -> Vec<DownloadInfo> {
self.list()
.into_iter()
.filter(|d| d.status == DownloadStatus::Completed)
.collect()
}
pub fn clear(&self) {
if let Ok(mut l) = self.downloads.lock() {
l.clear();
}
}
pub async fn download_file(
client: &reqwest::Client,
url: &str,
save_path: &Path,
) -> Result<PathBuf> {
debug!("Downloading {url} to {}", save_path.display());
let resp = client.get(url).send().await.map_err(Error::Reqwest)?;
let _total_size = resp.content_length();
let bytes = resp.bytes().await.map_err(Error::Reqwest)?;
if let Some(parent) = save_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(save_path, &bytes)?;
info!(
"Downloaded {} bytes to {}",
bytes.len(),
save_path.display()
);
Ok(save_path.to_path_buf())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_download_manager_register_and_complete() {
let dm = DownloadManager::new();
let id = dm.register("https://example.com/file.zip", "file.zip");
let dl = dm.get(&id).unwrap();
assert_eq!(dl.status, DownloadStatus::InProgress);
assert_eq!(dl.filename, "file.zip");
dm.complete(&id, Path::new("/tmp/file.zip"));
let dl = dm.get(&id).unwrap();
assert_eq!(dl.status, DownloadStatus::Completed);
assert_eq!(dl.save_path, Some(PathBuf::from("/tmp/file.zip")));
assert_eq!(dm.completed().len(), 1);
}
#[test]
fn test_download_manager_fail() {
let dm = DownloadManager::new();
let id = dm.register("https://example.com/file.zip", "file.zip");
dm.fail(&id, "connection reset");
let dl = dm.get(&id).unwrap();
assert!(matches!(dl.status, DownloadStatus::Failed(_)));
}
#[test]
fn test_download_manager_clear() {
let dm = DownloadManager::new();
dm.register("https://example.com/a.zip", "a.zip");
dm.register("https://example.com/b.zip", "b.zip");
assert_eq!(dm.list().len(), 2);
dm.clear();
assert!(dm.list().is_empty());
}
}