1mod cdn;
3pub mod queue;
4
5use crate::api::{FileManifest, GameManifest};
6use crate::{Error, Result};
7use std::fs;
8use std::io::Write;
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::Arc;
12use tokio::sync::Mutex;
13
14pub use cdn::{CdnDownloader, DownloadStats};
15pub use queue::{DownloadPriority, DownloadQueue, DownloadStatus, QueueItem, ScheduleWindow};
16
17pub struct DownloadManager {
18 cdn: CdnDownloader,
19 _chunk_cache_dir: PathBuf,
20}
21
22#[derive(Debug, Clone)]
23pub struct DownloadProgress {
24 pub total_bytes: u64,
25 pub downloaded_bytes: u64,
26 pub total_files: usize,
27 pub downloaded_files: usize,
28 pub current_file: String,
29 pub download_speed: f64,
30}
31
32impl DownloadManager {
33 pub fn new(cache_dir: PathBuf) -> Result<Self> {
34 Self::with_bandwidth_limit(cache_dir, None)
35 }
36
37 pub fn with_bandwidth_limit(
38 cache_dir: PathBuf,
39 bandwidth_limit_mbps: Option<u32>,
40 ) -> Result<Self> {
41 fs::create_dir_all(&cache_dir)?;
42
43 let cdn = CdnDownloader::with_bandwidth_limit(cache_dir.clone(), bandwidth_limit_mbps)?;
44
45 Ok(Self {
46 cdn,
47 _chunk_cache_dir: cache_dir,
48 })
49 }
50 pub async fn download_game(
52 &self,
53 manifest: &GameManifest,
54 install_dir: &Path,
55 progress_callback: impl Fn(DownloadProgress) + Send + Sync + 'static,
56 cancel_flag: Option<Arc<AtomicBool>>,
57 ) -> Result<()> {
58 let total_bytes = manifest.build_size;
59 let total_files = manifest.file_list.len();
60
61 let progress = Arc::new(Mutex::new(DownloadProgress {
62 total_bytes,
63 downloaded_bytes: 0,
64 total_files,
65 downloaded_files: 0,
66 current_file: String::new(),
67 download_speed: 0.0,
68 }));
69
70 fs::create_dir_all(install_dir)?;
72
73 for (idx, file_manifest) in manifest.file_list.iter().enumerate() {
75 if let Some(flag) = cancel_flag.as_ref() {
76 if flag.load(Ordering::Relaxed) {
77 return Err(Error::Other("Download cancelled".to_string()));
78 }
79 }
80
81 let file_path = install_dir.join(&file_manifest.filename);
82 let file_size: u64 = file_manifest.file_chunk_parts.iter().map(|c| c.size).sum();
83
84 {
86 let mut prog = progress.lock().await;
87 prog.current_file = file_manifest.filename.clone();
88 prog.downloaded_files = idx;
89 progress_callback(prog.clone());
90 }
91
92 if let Some(parent) = file_path.parent() {
94 fs::create_dir_all(parent)?;
95 }
96
97 self.download_file(
99 file_manifest,
100 &file_path,
101 &manifest.chunk_hash_list,
102 cancel_flag.clone(),
103 )
104 .await?;
105
106 {
108 let mut prog = progress.lock().await;
109 prog.downloaded_files = idx + 1;
110 prog.downloaded_bytes = prog.downloaded_bytes.saturating_add(file_size);
111 progress_callback(prog.clone());
112 }
113 }
114
115 Ok(())
116 }
117
118 async fn download_file(
119 &self,
120 file_manifest: &FileManifest,
121 output_path: &Path,
122 chunk_hashes: &std::collections::HashMap<String, String>,
123 cancel_flag: Option<Arc<AtomicBool>>,
124 ) -> Result<()> {
125 let mut output_file = fs::File::create(output_path)?;
126
127 let chunks_to_download: Vec<(String, String)> = file_manifest
129 .file_chunk_parts
130 .iter()
131 .filter_map(|chunk_part| {
132 chunk_hashes
133 .get(&chunk_part.guid)
134 .map(|hash| (chunk_part.guid.clone(), hash.clone()))
135 })
136 .collect();
137
138 let chunk_data_list = self
140 .cdn
141 .download_chunks_parallel(
142 chunks_to_download,
143 |_stats| {
144 },
146 cancel_flag,
147 )
148 .await?;
149
150 for chunk_data in chunk_data_list {
152 output_file.write_all(&chunk_data)?;
153 }
154
155 Ok(())
156 }
157 pub fn verify_file(file_path: &Path, expected_hash: &[u8]) -> Result<bool> {
159 use sha2::{Digest, Sha256};
160
161 let file_data = fs::read(file_path)?;
162 let mut hasher = Sha256::new();
163 hasher.update(&file_data);
164 let hash = hasher.finalize();
165
166 Ok(hash.as_slice() == expected_hash)
167 }
168
169 pub async fn resume_download(
171 &self,
172 manifest: &GameManifest,
173 install_dir: &Path,
174 progress_callback: impl Fn(DownloadProgress) + Send + Sync + 'static,
175 cancel_flag: Option<Arc<AtomicBool>>,
176 ) -> Result<()> {
177 let mut _downloaded_bytes = 0u64;
179 let mut downloaded_files = 0;
180
181 for file_manifest in &manifest.file_list {
182 let file_path = install_dir.join(&file_manifest.filename);
183
184 if file_path.exists() && Self::verify_file(&file_path, &file_manifest.file_hash)? {
185 downloaded_files += 1;
186 let file_size: u64 = file_manifest.file_chunk_parts.iter().map(|c| c.size).sum();
188 _downloaded_bytes += file_size;
189 }
190 }
191
192 log::info!(
193 "Resuming download: {} files already complete",
194 downloaded_files
195 );
196
197 self.download_game(manifest, install_dir, progress_callback, cancel_flag)
199 .await
200 }
201}