Downloader

This commit is contained in:
2026-04-04 07:40:44 +02:00
parent 8985be3ad3
commit f751134303
5 changed files with 206 additions and 4 deletions
+200
View File
@@ -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<u64, Error>`. 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<P: AsRef<Path>>(
url: impl IntoUrl,
destination: P,
emitter: Option<&Emitter>,
client: Option<&Client>,
) -> lyceris::Result<u64> {
// 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<U, P>(
downloads: Vec<(U, P, FileType)>,
emitter: Option<&Emitter>,
client: Option<&Client>,
) -> lyceris::Result<()>
where
U: IntoUrl + Send, // URL type that implements IntoUrl
P: AsRef<Path> + 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(())
}
+3 -4
View File
@@ -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 {
+1
View File
@@ -1,3 +1,4 @@
pub mod downloader;
pub mod install;
use directories::ProjectDirs;