| 1 | //! WeMod download functionality |
| 2 | |
| 3 | use crate::config::WandaConfig; |
| 4 | use crate::error::{Result, WandaError}; |
| 5 | use futures::StreamExt; |
| 6 | use std::path::PathBuf; |
| 7 | use tokio::fs::File; |
| 8 | use tokio::io::AsyncWriteExt; |
| 9 | use tracing::{debug, info}; |
| 10 | |
| 11 | /// WeMod download API endpoint |
| 12 | const WEMOD_DOWNLOAD_URL: &str = "https://api.wemod.com/client/download"; |
| 13 | |
| 14 | /// Information about a WeMod release |
| 15 | #[derive(Debug, Clone)] |
| 16 | pub struct WemodRelease { |
| 17 | /// Download URL |
| 18 | pub url: String, |
| 19 | /// Version string (if known) |
| 20 | pub version: Option<String>, |
| 21 | /// File size in bytes (if known) |
| 22 | pub size: Option<u64>, |
| 23 | } |
| 24 | |
| 25 | /// Handles downloading WeMod |
| 26 | pub struct WemodDownloader { |
| 27 | /// HTTP client |
| 28 | client: reqwest::Client, |
| 29 | /// Cache directory for downloads |
| 30 | cache_dir: PathBuf, |
| 31 | } |
| 32 | |
| 33 | impl WemodDownloader { |
| 34 | /// Create a new downloader |
| 35 | pub fn new(_config: &WandaConfig) -> Self { |
| 36 | Self { |
| 37 | client: reqwest::Client::builder() |
| 38 | .user_agent("WANDA/0.1.0") |
| 39 | .build() |
| 40 | .expect("Failed to create HTTP client"), |
| 41 | cache_dir: WandaConfig::default_cache_dir().join("downloads"), |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | /// Get the latest WeMod release info |
| 46 | pub async fn get_latest(&self) -> Result<WemodRelease> { |
| 47 | info!("Fetching WeMod download info..."); |
| 48 | |
| 49 | // The WeMod API redirects to the actual download URL |
| 50 | // We need to follow the redirect to get the final URL |
| 51 | let response = self |
| 52 | .client |
| 53 | .head(WEMOD_DOWNLOAD_URL) |
| 54 | .send() |
| 55 | .await |
| 56 | .map_err(|e| WandaError::WemodDownloadFailed { |
| 57 | reason: format!("Failed to fetch download info: {}", e), |
| 58 | })?; |
| 59 | |
| 60 | let final_url = response.url().to_string(); |
| 61 | let content_length = response |
| 62 | .headers() |
| 63 | .get(reqwest::header::CONTENT_LENGTH) |
| 64 | .and_then(|v| v.to_str().ok()) |
| 65 | .and_then(|v| v.parse().ok()); |
| 66 | |
| 67 | // Try to extract version from URL |
| 68 | // URL format is typically: https://storage.wemod.com/app/releases/stable/WeMod-X.Y.Z.exe |
| 69 | let version = final_url |
| 70 | .split('/') |
| 71 | .last() |
| 72 | .and_then(|filename| { |
| 73 | filename |
| 74 | .strip_prefix("WeMod-") |
| 75 | .and_then(|v| v.strip_suffix(".exe")) |
| 76 | .map(String::from) |
| 77 | }); |
| 78 | |
| 79 | debug!("WeMod download URL: {}", final_url); |
| 80 | if let Some(ref v) = version { |
| 81 | debug!("Detected version: {}", v); |
| 82 | } |
| 83 | |
| 84 | Ok(WemodRelease { |
| 85 | url: final_url, |
| 86 | version, |
| 87 | size: content_length, |
| 88 | }) |
| 89 | } |
| 90 | |
| 91 | /// Download WeMod installer to cache |
| 92 | pub async fn download<F>(&self, release: &WemodRelease, progress_callback: F) -> Result<PathBuf> |
| 93 | where |
| 94 | F: Fn(u64, u64), |
| 95 | { |
| 96 | // Ensure cache directory exists |
| 97 | tokio::fs::create_dir_all(&self.cache_dir).await?; |
| 98 | |
| 99 | let filename = release |
| 100 | .version |
| 101 | .as_ref() |
| 102 | .map(|v| format!("WeMod-{}.exe", v)) |
| 103 | .unwrap_or_else(|| "WeMod-latest.exe".to_string()); |
| 104 | |
| 105 | let dest_path = self.cache_dir.join(&filename); |
| 106 | |
| 107 | // Check if we already have this version cached |
| 108 | if dest_path.exists() { |
| 109 | if let Some(expected_size) = release.size { |
| 110 | let actual_size = tokio::fs::metadata(&dest_path).await?.len(); |
| 111 | if actual_size == expected_size { |
| 112 | info!("Using cached WeMod installer: {}", dest_path.display()); |
| 113 | return Ok(dest_path); |
| 114 | } |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | info!("Downloading WeMod from {}...", release.url); |
| 119 | |
| 120 | let response = self |
| 121 | .client |
| 122 | .get(&release.url) |
| 123 | .send() |
| 124 | .await |
| 125 | .map_err(|e| WandaError::WemodDownloadFailed { |
| 126 | reason: format!("Download request failed: {}", e), |
| 127 | })?; |
| 128 | |
| 129 | if !response.status().is_success() { |
| 130 | return Err(WandaError::WemodDownloadFailed { |
| 131 | reason: format!("HTTP error: {}", response.status()), |
| 132 | }); |
| 133 | } |
| 134 | |
| 135 | let total_size = response.content_length().unwrap_or(0); |
| 136 | let mut downloaded: u64 = 0; |
| 137 | |
| 138 | let mut file = File::create(&dest_path).await?; |
| 139 | let mut stream = response.bytes_stream(); |
| 140 | |
| 141 | while let Some(chunk_result) = stream.next().await { |
| 142 | let chunk = chunk_result.map_err(|e| WandaError::WemodDownloadFailed { |
| 143 | reason: format!("Error reading download stream: {}", e), |
| 144 | })?; |
| 145 | |
| 146 | file.write_all(&chunk).await?; |
| 147 | |
| 148 | downloaded += chunk.len() as u64; |
| 149 | progress_callback(downloaded, total_size); |
| 150 | } |
| 151 | |
| 152 | file.flush().await?; |
| 153 | |
| 154 | info!("Downloaded WeMod to {}", dest_path.display()); |
| 155 | Ok(dest_path) |
| 156 | } |
| 157 | |
| 158 | /// Check if we have a cached version matching the release |
| 159 | pub async fn get_cached(&self, release: &WemodRelease) -> Option<PathBuf> { |
| 160 | if let Some(ref version) = release.version { |
| 161 | let path = self.cache_dir.join(format!("WeMod-{}.exe", version)); |
| 162 | if path.exists() { |
| 163 | return Some(path); |
| 164 | } |
| 165 | } |
| 166 | None |
| 167 | } |
| 168 | |
| 169 | /// Clear the download cache |
| 170 | pub async fn clear_cache(&self) -> Result<()> { |
| 171 | if self.cache_dir.exists() { |
| 172 | tokio::fs::remove_dir_all(&self.cache_dir).await?; |
| 173 | } |
| 174 | Ok(()) |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | #[cfg(test)] |
| 179 | mod tests { |
| 180 | use super::*; |
| 181 | |
| 182 | #[test] |
| 183 | fn test_version_extraction() { |
| 184 | let url = "https://storage.wemod.com/app/releases/stable/WeMod-8.12.5.exe"; |
| 185 | let version = url |
| 186 | .split('/') |
| 187 | .last() |
| 188 | .and_then(|f| f.strip_prefix("WeMod-")) |
| 189 | .and_then(|v| v.strip_suffix(".exe")); |
| 190 | |
| 191 | assert_eq!(version, Some("8.12.5")); |
| 192 | } |
| 193 | } |
| 194 |