1
2use zip::ZipArchive;
3use std::sync::Arc;
4use tracing::{error, info, warn};
5use std::path::PathBuf;
6use tokio::fs;
7use tokio::io::{AsyncWriteExt, BufWriter};
8use tokio::sync::Semaphore;
9use lighty_loaders::version::Version;
10use lighty_version::version_metadata::{Library, Native, Client, AssetsFile, VersionBuilder,Mods};
11use lighty_core::hosts::HTTP_CLIENT as CLIENT;
12use lighty_loaders::utils::sha1::verify_file_sha1;
13use lighty_core::{mkdir, time_it};
14use futures::future::try_join_all;
15use futures::StreamExt;
16use crate::errors::{InstallerError, InstallerResult};
17
18const MAX_CONCURRENT_DOWNLOADS: usize = 50;
20
21pub trait Installer {
22 async fn install(&self , builder: &VersionBuilder) -> InstallerResult<()>;
23}
24
25impl<'a> Installer for Version<'a> {
26 async fn install(&self , builder: &VersionBuilder) -> InstallerResult<()> {
28 info!("[Installer] Starting installation for {}", self.name);
29
30 time_it!("Total installation", {
31 create_directories(self).await;
32
33
34 tokio::try_join!(
36 verify_and_download_libraries(self, &builder.libraries),
37 verify_and_download_natives(self, builder.natives.as_deref().unwrap_or(&[])),
39 verify_and_download_mods(self, builder.mods.as_deref().unwrap_or(&[])),
40 verify_and_download_client(self, builder.client.as_ref()),
41 verify_and_download_assets(self, builder.assets.as_ref()),
42 )?;
43 });
44
45 info!("[Installer] Installation completed successfully!");
46 Ok(())
47 }
48}
49
50async fn create_directories<'a>(version: &Version<'a>) {
51 let parent_path = version.game_dirs.to_path_buf();
52 mkdir!(parent_path.join("libraries"));
53 mkdir!(parent_path.join("natives"));
54 mkdir!(parent_path.join("assets").join("objects"));
55}
56
57async fn verify_and_download_libraries<'a>(version: &Version<'a>, libraries: &[Library]) -> InstallerResult<()> {
59 let parent_path = version.game_dirs.join("libraries");
60 let mut tasks = Vec::new();
61
62 for lib in libraries {
63 let Some(url) = &lib.url else { continue };
64 let Some(path_str) = &lib.path else { continue };
65
66 let path = parent_path.join(path_str);
67
68 let needs_download = if !path.exists() {
69 true
70 } else if let Some(sha1) = &lib.sha1 {
71 match verify_file_sha1(&path, sha1).await {
72 Ok(true) => false,
73 _ => {
74 warn!("[Installer] SHA1 mismatch for {}, re-downloading...", lib.name);
75 let _ = fs::remove_file(&path).await;
76 true
77 }
78 }
79 } else {
80 false
81 };
82
83 if needs_download {
84 tasks.push((url.clone(), path));
85 }
86 }
87
88 if tasks.is_empty() {
89 info!("[Installer] ✓ All libraries already cached and verified");
90 return Ok(());
91 }
92
93 info!("[Installer] Downloading {} libraries...", tasks.len());
94 time_it!("Libraries download", {
95 download_with_concurrency_limit(tasks).await?
96 });
97 info!("[Installer] ✓ Libraries installed");
98 Ok(())
99}
100
101async fn verify_and_download_mods<'a>(version: &Version<'a>, mods: &[Mods]) -> InstallerResult<()> {
102 let parent_path = version.game_dirs.join("mods");
103 let mut tasks = Vec::new();
104
105 for _mod in mods {
106 let Some(url) = &_mod.url else { continue };
107 let Some(path_str) = &_mod.path else { continue };
108
109 let path = parent_path.join(path_str);
110
111 let needs_download = if !path.exists() {
112 true
113 } else if let Some(sha1) = &_mod.sha1 {
114 match verify_file_sha1(&path, sha1).await {
115 Ok(true) => false,
116 _ => {
117 warn!("[Installer] SHA1 mismatch for {}, re-downloading...", _mod.name);
118 let _ = fs::remove_file(&path).await;
119 true
120 }
121 }
122 } else {
123 false
124 };
125
126 if needs_download {
127 tasks.push((url.clone(), path));
128 }
129 }
130
131 if tasks.is_empty() {
132 info!("[Installer] ✓ All Mod already cached and verified");
133 return Ok(());
134 }
135
136 info!("[Installer] Downloading {} libraries...", tasks.len());
137 time_it!("Libraries download", {
138 download_with_concurrency_limit(tasks).await?
139 });
140 info!("[Installer] ✓ Libraries installed");
141 Ok(())
142}
143
144async fn verify_and_download_natives<'a>(version: &Version<'a>, natives: &[Native]) -> InstallerResult<()> {
146 if natives.is_empty() {
147 return Ok(());
148 }
149
150 let libraries_path = version.game_dirs.join("libraries");
151 let natives_extract_path = version.game_dirs.join("natives");
152
153 if natives_extract_path.exists() {
155 let _ = fs::remove_dir_all(&natives_extract_path).await;
156 }
157 mkdir!(natives_extract_path);
158
159 let mut download_tasks = Vec::new();
161 let mut extract_paths = Vec::new();
162
163 for native in natives {
164 let Some(url) = &native.url else { continue };
165 let Some(path_str) = &native.path else { continue };
166
167 let jar_path = libraries_path.join(path_str);
168
169 let needs_download = if !jar_path.exists() {
171 true
172 } else if let Some(sha1) = &native.sha1 {
173 match verify_file_sha1(&jar_path, sha1).await {
174 Ok(true) => false,
175 _ => {
176 warn!("[Installer] SHA1 mismatch for {}, re-downloading...", native.name);
177 let _ = fs::remove_file(&jar_path).await;
178 true
179 }
180 }
181 } else {
182 false
183 };
184
185 if needs_download {
186 download_tasks.push((url.clone(), jar_path.clone()));
187 }
188
189 extract_paths.push(jar_path);
190 }
191
192 if !download_tasks.is_empty() {
194 info!("[Installer] Downloading {} natives...", download_tasks.len());
195 time_it!("Natives download", {
196 download_with_concurrency_limit(download_tasks).await?
197 });
198 info!("[Installer] ✓ Natives downloaded");
199 } else {
200 info!("[Installer] ✓ All natives already cached and verified");
201 }
202
203 if !extract_paths.is_empty() {
205 info!("[Installer] Extracting {} natives...", extract_paths.len());
206 let extraction_tasks: Vec<_> = extract_paths
207 .into_iter()
208 .map(|jar_path| extract_native(jar_path, natives_extract_path.clone()))
209 .collect();
210
211 time_it!("Natives extraction", try_join_all(extraction_tasks).await?);
212 info!("[Installer] ✓ Natives extracted");
213 }
214
215 Ok(())
216}
217
218async fn verify_and_download_client<'a>(version: &Version<'a>, client: Option<&Client>) -> InstallerResult<()> {
220 let Some(client) = client else {
221 return Ok(());
222 };
223
224 let Some(url) = &client.url else {
225 return Ok(());
226 };
227
228 let client_path = version.game_dirs.join(format!("{}.jar", version.name));
229
230 let needs_download = if !client_path.exists() {
231 true
232 } else if let Some(sha1) = &client.sha1 {
233 match verify_file_sha1(&client_path, sha1).await {
234 Ok(true) => false,
235 _ => {
236 warn!("[Installer] Client JAR SHA1 mismatch, re-downloading...");
237 let _ = fs::remove_file(&client_path).await;
238 true
239 }
240 }
241 } else {
242 false
243 };
244
245 if !needs_download {
246 info!("[Installer] ✓ Client JAR already cached and verified");
247 return Ok(());
248 }
249
250 info!("[Installer] Downloading client JAR...");
251 time_it!("Client download", download_large_file(url.clone(), client_path).await?);
252 info!("[Installer] ✓ Client JAR installed");
253 Ok(())
254}
255
256async fn verify_and_download_assets<'a>(version: &Version<'a>, assets: Option<&AssetsFile>) -> InstallerResult<()> {
258 let Some(assets) = assets else {
259 return Ok(());
260 };
261
262 let parent_path = version.game_dirs.join("assets").join("objects");
263 let mut tasks = Vec::new();
264
265 for asset in assets.objects.values() {
266 let Some(url) = &asset.url else { continue };
267
268 let hash_prefix = &asset.hash[0..2];
269 let path = parent_path.join(hash_prefix).join(&asset.hash);
270
271 let needs_download = if !path.exists() {
272 true
273 } else {
274 match verify_file_sha1(&path, &asset.hash).await {
275 Ok(true) => false,
276 _ => {
277 let _ = fs::remove_file(&path).await;
278 true
279 }
280 }
281 };
282
283 if needs_download {
284 tasks.push((url.clone(), path));
285 }
286 }
287
288 if tasks.is_empty() {
289 info!("[Installer] ✓ All assets already cached and verified");
290 return Ok(());
291 }
292
293 info!("[Installer] Downloading {} new assets...", tasks.len());
294 time_it!("Assets download", {
295 download_small_with_concurrency_limit(tasks).await?
296 });
297 info!("[Installer] ✓ Assets installed");
298 Ok(())
299}
300
301async fn extract_native(jar_path: PathBuf, natives_dir: PathBuf) -> InstallerResult<()> {
303 tokio::task::spawn_blocking(move || {
305 let file = std::fs::File::open(&jar_path)?;
307 let mmap = unsafe { memmap2::Mmap::map(&file)? };
309
310 let cursor = std::io::Cursor::new(&mmap[..]);
311 let mut archive = ZipArchive::new(cursor)?;
312
313 for i in 0..archive.len() {
315 let mut file = archive.by_index(i)?;
316 let file_name = file.name().to_string();
317
318 if is_native_file(&file_name) {
319 let dest_path = natives_dir.join(
320 std::path::Path::new(&file_name)
321 .file_name()
322 .unwrap_or_default()
323 );
324
325 let mut dest_file = std::fs::File::create(&dest_path)?;
327 std::io::copy(&mut file, &mut dest_file)?;
328 }
329 }
330
331 Ok::<_, InstallerError>(())
332 })
333 .await
334 .map_err(|e| InstallerError::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?
335}
336
337#[inline]
339fn is_native_file(filename: &str) -> bool {
340 const NATIVE_EXTENSIONS: &[&str] = &[".dll", ".so", ".dylib", ".jnilib"];
341
342 let filename_lower = filename.to_lowercase();
343
344 NATIVE_EXTENSIONS.iter().any(|ext| filename_lower.ends_with(ext))
345 || filename_lower.contains(".so.")
346}
347
348async fn download_small_file(url: String, dest: PathBuf) -> InstallerResult<()> {
350 const MAX_RETRIES: u32 = 3;
351 const INITIAL_DELAY_MS: u64 = 20;
352
353 let mut last_error = None;
354
355 for attempt in 1..=MAX_RETRIES {
356 match download_small_file_once(&url, &dest).await {
357 Ok(_) => return Ok(()),
358 Err(e) => {
359 if attempt < MAX_RETRIES {
360 let delay = INITIAL_DELAY_MS * 2u64.pow(attempt - 1);
361 warn!(
362 "[Retry {}/{}] Failed to download {}: {}. Retrying in {}ms...",
363 attempt, MAX_RETRIES, url, e, delay
364 );
365 tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
366 }
367 last_error = Some(e);
368 }
369 }
370 }
371
372 Err(last_error.unwrap())
373}
374
375async fn download_small_file_once(url: &str, dest: &PathBuf) -> InstallerResult<()> {
376 let bytes = CLIENT.get(url).send().await?.bytes().await?;
377
378 if let Some(parent) = dest.parent() {
379 mkdir!(parent);
380 }
381
382 fs::write(dest, bytes).await?;
383 Ok(())
384}
385
386async fn download_large_file(url: String, dest: PathBuf) -> InstallerResult<()> {
388 const MAX_RETRIES: u32 = 3;
389 const INITIAL_DELAY_MS: u64 = 20;
390
391 let mut last_error = None;
392
393 for attempt in 1..=MAX_RETRIES {
394 match download_large_file_once(&url, &dest).await {
395 Ok(_) => return Ok(()),
396 Err(e) => {
397 if attempt < MAX_RETRIES {
398 let delay = INITIAL_DELAY_MS * 2u64.pow(attempt - 1);
399 warn!(
400 "[Retry {}/{}] Failed to download {}: {}. Retrying in {}ms...",
401 attempt, MAX_RETRIES, url, e, delay
402 );
403 let _ = fs::remove_file(&dest).await;
404 tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
405 }
406 last_error = Some(e);
407 }
408 }
409 }
410
411 Err(last_error.unwrap())
412}
413
414async fn download_large_file_once(url: &str, dest: &PathBuf) -> InstallerResult<()> {
415 let response = CLIENT.get(url).send().await?;
416
417 if !response.status().is_success() {
418 return Err(InstallerError::DownloadFailed(format!(
419 "HTTP {} for {}",
420 response.status(),
421 url
422 )));
423 }
424
425 if let Some(parent) = dest.parent() {
426 mkdir!(parent);
427 }
428
429 let file = fs::File::create(dest).await?;
430 let mut writer = BufWriter::with_capacity(256 * 1024, file);
431 let mut stream = response.bytes_stream();
432
433 while let Some(chunk) = stream.next().await {
434 let chunk = chunk?;
435 writer.write_all(&chunk).await?;
436 }
437
438 writer.flush().await?;
439 Ok(())
440}
441
442async fn download_with_concurrency_limit(tasks: Vec<(String, PathBuf)>) -> InstallerResult<()> {
444 let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_DOWNLOADS));
445 let futures: Vec<_> = tasks
446 .into_iter()
447 .map(|(url, dest)| {
448 let sem = semaphore.clone();
449 async move {
450 let _permit = sem.acquire().await.unwrap();
451 download_large_file(url, dest).await
452 }
453 })
454 .collect();
455
456 try_join_all(futures).await?;
457 Ok(())
458}
459
460async fn download_small_with_concurrency_limit(tasks: Vec<(String, PathBuf)>) -> InstallerResult<()> {
462 let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_DOWNLOADS));
463 let futures: Vec<_> = tasks
464 .into_iter()
465 .map(|(url, dest)| {
466 let sem = semaphore.clone();
467 async move {
468 let _permit = sem.acquire().await.unwrap();
469 download_small_file(url, dest).await
470 }
471 })
472 .collect();
473
474 try_join_all(futures).await?;
475 Ok(())
476}