Rust · 5899 bytes Raw Blame History
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