lighty_launch/
installer.rs

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
18// Limit concurrent downloads to prevent socket exhaustion
19const 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    /// Installe toutes les dépendances en parallèle
27    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            // Vérifier et télécharger en parallèle
35            tokio::try_join!(
36                verify_and_download_libraries(self, &builder.libraries),
37                //TODO revoir les unwraps
38                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
57/// Vérifie et télécharge les libraries manquantes/corrompues
58async 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
144/// Vérifie, télécharge et extrait les natives (clean systématique + extraction parallèle async)
145async 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        // Clean du dossier natives à chaque installation
154        if natives_extract_path.exists() {
155            let _ = fs::remove_dir_all(&natives_extract_path).await;
156        }
157        mkdir!(natives_extract_path);
158
159        // ✅ Séparer en deux passes : téléchargement puis extraction
160        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            // Vérifier si le JAR existe et est valide
170            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        // Télécharger les natives manquantes
193        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        // Extraire tous les natives en parallèle
204        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
218/// Vérifie et télécharge le client JAR si nécessaire
219async 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
256/// Vérifie et télécharge les assets manquants/corrompus
257async 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
301/// Extrait un native JAR (version memory-mapped avec spawn_blocking)
302async fn extract_native(jar_path: PathBuf, natives_dir: PathBuf) -> InstallerResult<()> {
303    // Use spawn_blocking for sync ZIP operations
304    tokio::task::spawn_blocking(move || {
305        // Memory-map the file instead of reading entirely into memory
306        let file = std::fs::File::open(&jar_path)?;
307        //TODO: REVOIR CE UNSAFE
308        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        // Extract native files
314        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                // Stream directly to disk instead of buffering in memory
326                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/// Vérifie si un fichier est natif
338#[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
348/// Téléchargement de petits fichiers
349async 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
386/// Téléchargement de gros fichiers
387async 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
442/// Download large files with concurrency limit
443async 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
460/// Download small files with concurrency limit
461async 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}