From f7511343031c22673b21f7829ec110ed792c7fcf Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 4 Apr 2026 07:40:44 +0200 Subject: [PATCH] Downloader --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/launcher/downloader.rs | 200 +++++++++++++++++++++++++++ src-tauri/src/launcher/install.rs | 7 +- src-tauri/src/launcher/mod.rs | 1 + 5 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 src-tauri/src/launcher/downloader.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8c125ae..b183c09 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3909,6 +3909,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", + "tokio", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index be44c79..3c502cc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,3 +28,4 @@ lyceris = "1.1.3" rayon = "1.10.0" reqwest = "0.12.9" futures = "0.3" +tokio = { version = "1.51.0", features = ["fs"] } diff --git a/src-tauri/src/launcher/downloader.rs b/src-tauri/src/launcher/downloader.rs new file mode 100644 index 0000000..9212162 --- /dev/null +++ b/src-tauri/src/launcher/downloader.rs @@ -0,0 +1,200 @@ +use futures::{stream, StreamExt}; +use reqwest::{Client, IntoUrl}; +use std::{ + path::Path, + sync::Arc, + time::{Duration, Instant}, +}; +use tokio::{ + fs::{create_dir_all, File}, + io::AsyncWriteExt, + sync::Mutex, + time::timeout, +}; + +use lyceris::{ + error::Error, + minecraft::{ + emitter::{Emit, Emitter, Event}, + install::FileType, + }, + util::retry::retry, +}; + +/// Downloads a file from the specified URL and saves it to the given destination. +/// +/// This function performs an asynchronous HTTP GET request to the provided URL, +/// streams the response body, and writes the content to a file at the specified +/// destination. It also provides progress updates through a callback function. +/// +/// # Parameters +/// +/// - `url`: The URL of the file to download. It can be any type that implements +/// the `IntoUrl` trait, such as a string slice or a `String`. +/// - `destination`: A `PathBuf` representing the path where the downloaded file +/// will be saved. +/// - `emitter`: An optional emitter for logging progress. +/// +/// # Returns +/// +/// This function returns a `Result`. On success, it returns +/// `Ok(u64)`, where `u64` is the total size of the downloaded file. If an error +/// occurs during the download process, it returns an `Err` containing an `Error` +/// that describes the failure. +/// +/// # Errors +/// +/// The function can fail in several ways, including but not limited to: +/// - Network errors when making the HTTP request. +/// - Non-success HTTP status codes (e.g., 404 Not Found). +/// - Errors when creating or writing to the file. +pub async fn download>( + url: impl IntoUrl, + destination: P, + emitter: Option<&Emitter>, + client: Option<&Client>, +) -> lyceris::Result { + // Send a get request to the given url. + let default_client = Client::default(); + let client = client.unwrap_or(&default_client); + let response = client.get(url).send().await?; + + if !response.status().is_success() { + return Err(Error::Download(response.status().to_string())); + } + + // Get the total size of the file to use at progression + let total_size = response.content_length().unwrap_or(0); + let mut downloaded: u64 = 0; + + if let Some(parent) = destination.as_ref().parent() { + if !parent.is_dir() { + create_dir_all(parent).await?; + } + } + + // Create a file to write the downloaded content + let mut file = File::create(&destination).await?; + + // Stream the response body + let mut stream = response.bytes_stream(); + + let mut last_data_received; + + while let Some(chunk_result) = timeout(Duration::from_secs(10), stream.next()).await? { + match chunk_result { + Ok(chunk) => { + // Reset the timer when data is received + last_data_received = Instant::now(); + downloaded += chunk.len() as u64; + + // Write chunk to the file + file.write_all(&chunk).await?; + + // Emit progress event + emitter + .emit( + Event::SingleDownloadProgress, + ( + destination.as_ref().to_string_lossy().into_owned(), + downloaded, + total_size, + ), + ) + .await; + } + Err(_) => { + // Timeout occurred (no chunk received in 3 seconds) + return Err(Error::Download( + "Connection dead, no data for 3 seconds.".to_string(), + )); + } + } + + // Check if no data has been received in the last 3 seconds + if last_data_received.elapsed() > Duration::from_secs(10) { + return Err(Error::Download( + "Connection dead, no data for 3 seconds.".to_string(), + )); + } + } + + Ok(total_size) +} + +/// Downloads multiple files from the specified URLs and saves them to the given destinations. +/// +/// This function takes a vector of tuples, where each tuple contains a URL and a destination path. +/// It downloads all files in parallel and provides progress updates through a callback function. +/// +/// # Parameters +/// +/// - `downloads`: A vector of tuples containing the URLs and their corresponding destination paths. +/// - `emitter`: An optional emitter for logging progress. +/// +/// # Returns +/// +/// This function returns a `Result<(), Error>`. On success, it returns `Ok(())`. If an error occurs +/// during the download process, it returns an `Err` containing an `Error` that describes the failure. +pub async fn download_multiple( + downloads: Vec<(U, P, FileType)>, + emitter: Option<&Emitter>, + client: Option<&Client>, +) -> lyceris::Result<()> +where + U: IntoUrl + Send, // URL type that implements IntoUrl + P: AsRef + Send + 'static, // Path type +{ + let total_files = downloads.len(); + let total_downloaded = Arc::new(Mutex::new(0)); + let tasks = downloads.into_iter().map(|(url, destination, file_type)| { + let total_downloaded = Arc::clone(&total_downloaded); + async move { + // Retry download logic + let result = retry( + || async { download(url.as_str(), destination.as_ref(), emitter, client).await }, + Result::is_ok, + 3, + Duration::from_secs(5), + ) + .await; + + // Check if the download was successful + match result { + Ok(_) => { + // Update the progress counter + let mut downloaded = total_downloaded.lock().await; + *downloaded += 1; + + emitter + .emit( + Event::MultipleDownloadProgress, + ( + destination.as_ref().to_string_lossy().into_owned(), + *downloaded as u64, + total_files as u64, + file_type.to_string(), + ), + ) + .await; + + Ok::<(), Error>(()) + } + Err(e) => { + // Return the error immediately + Err(e) + } + } + } + }); + + // Create a stream of tasks with limited concurrency + let mut stream = stream::iter(tasks).buffered(10); // Limit concurrency here + + // Poll the stream and handle results + while let Some(result) = stream.next().await { + result?; + } + + Ok(()) +} diff --git a/src-tauri/src/launcher/install.rs b/src-tauri/src/launcher/install.rs index a19ddf8..0dfd84b 100644 --- a/src-tauri/src/launcher/install.rs +++ b/src-tauri/src/launcher/install.rs @@ -11,10 +11,7 @@ use std::{ use lyceris::{ error::Error, - http::{ - downloader::{download, download_multiple}, - fetch::fetch, - }, + http::fetch::fetch, json::{ java::{JavaFileManifest, JavaManifest}, version::{ @@ -41,6 +38,8 @@ use lyceris::minecraft::{ parse::{parse_lib_path, ParseRule}, }; +use crate::launcher::downloader::{download, download_multiple}; + /// Represents a file to be downloaded, including its metadata. #[derive(Clone)] struct DownloadFile { diff --git a/src-tauri/src/launcher/mod.rs b/src-tauri/src/launcher/mod.rs index 784a5a8..1da4a8c 100644 --- a/src-tauri/src/launcher/mod.rs +++ b/src-tauri/src/launcher/mod.rs @@ -1,3 +1,4 @@ +pub mod downloader; pub mod install; use directories::ProjectDirs;