epik/downloader/
mod.rs

1// Download manager for Epic Games content
2mod 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    // Download all files for a game based on manifest
51    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        // Create install directory
71        fs::create_dir_all(install_dir)?;
72
73        // Download files
74        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            // Update progress
85            {
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            // Create parent directories
93            if let Some(parent) = file_path.parent() {
94                fs::create_dir_all(parent)?;
95            }
96
97            // Download and assemble file from chunks
98            self.download_file(
99                file_manifest,
100                &file_path,
101                &manifest.chunk_hash_list,
102                cancel_flag.clone(),
103            )
104            .await?;
105
106            // Update progress
107            {
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        // Collect chunks to download
128        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        // Download chunks in parallel
139        let chunk_data_list = self
140            .cdn
141            .download_chunks_parallel(
142                chunks_to_download,
143                |_stats| {
144                    // Progress callback - could be used to update UI
145                },
146                cancel_flag,
147            )
148            .await?;
149
150        // Write chunks to file
151        for chunk_data in chunk_data_list {
152            output_file.write_all(&chunk_data)?;
153        }
154
155        Ok(())
156    }
157    // Verify file integrity using SHA256 hash
158    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    // Resume interrupted download
170    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        // Check which files are already downloaded and verified
178        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                // Calculate file size from chunks
187                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        // Continue download for remaining files
198        self.download_game(manifest, install_dir, progress_callback, cancel_flag)
199            .await
200    }
201}