use std::path::{Path, PathBuf};
use std::sync::Arc;
use reqwest::header::RANGE;
use reqwest::StatusCode;
use sha1::{Digest, Sha1};
use tokio::fs;
use tokio::fs::OpenOptions;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter};
use tokio::sync::Semaphore;
use futures::future::try_join_all;
use futures::StreamExt;
use lighty_core::calculate_sha1_bytes;
use lighty_core::hosts::HTTP_CLIENT as CLIENT;
use lighty_core::mkdir;
use crate::errors::InstallerResult;
use crate::errors::InstallerError;
use super::config::get_config;
#[cfg(feature = "events")]
use lighty_event::{EventBus, Event, LaunchEvent};
pub struct DownloadTask<'a> {
pub url: &'a str,
pub dest: PathBuf,
pub sha1: Option<&'a str>,
pub size: u64,
}
fn calculate_retry_delay(base_delay_ms: u64, attempt: u32) -> u64 {
let exponential_delay = base_delay_ms * 2u64.pow(attempt - 1);
let jitter = fastrand::u64(0..=exponential_delay / 2);
exponential_delay + jitter
}
pub async fn download_small_file(
url: &str,
dest: &Path,
sha1: Option<&str>,
#[cfg(feature = "events")] event_bus: Option<&EventBus>,
) -> InstallerResult<()> {
let config = get_config();
let mut last_error = None;
for attempt in 1..=config.max_retries {
match download_small_file_once(
url,
dest,
sha1,
#[cfg(feature = "events")]
event_bus,
).await {
Ok(_) => return Ok(()),
Err(e) => {
if attempt < config.max_retries {
let delay = calculate_retry_delay(config.initial_delay_ms, attempt);
lighty_core::trace_warn!(
"[Retry {}/{}] Failed to download {}: {}. Retrying in {}ms...",
attempt, config.max_retries, url, e, delay
);
tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
}
last_error = Some(e);
}
}
}
Err(last_error.unwrap_or_else(|| InstallerError::RetriesExhausted {
attempts: config.max_retries,
url: url.to_string(),
}))
}
async fn download_small_file_once(
url: &str,
dest: &Path,
sha1: Option<&str>,
#[cfg(feature = "events")] event_bus: Option<&EventBus>,
) -> InstallerResult<()> {
let response = CLIENT.get(url).send().await?;
if !response.status().is_success() {
return Err(InstallerError::HttpStatus {
status: response.status().as_u16(),
url: url.to_string(),
});
}
let bytes = response.bytes().await?;
if let Some(expected) = sha1 {
let digest = calculate_sha1_bytes(&bytes);
if !digest.eq_ignore_ascii_case(expected) {
return Err(InstallerError::Sha1Mismatch {
url: url.to_string(),
expected: expected.to_string(),
actual: digest,
});
}
}
#[cfg(feature = "events")]
if let Some(bus) = event_bus {
bus.emit(Event::Launch(LaunchEvent::InstallProgress {
bytes: bytes.len() as u64,
}));
}
if let Some(parent) = dest.parent() {
mkdir!(parent);
}
fs::write(dest, bytes).await?;
Ok(())
}
pub async fn download_large_file(
url: &str,
dest: &Path,
sha1: Option<&str>,
#[cfg(feature = "events")] event_bus: Option<&EventBus>,
) -> InstallerResult<()> {
let config = get_config();
let mut last_error = None;
for attempt in 1..=config.max_retries {
match download_large_file_once(
url,
dest,
sha1,
#[cfg(feature = "events")]
event_bus,
)
.await
{
Ok(_) => return Ok(()),
Err(e) => {
if attempt < config.max_retries {
let delay = calculate_retry_delay(config.initial_delay_ms, attempt);
lighty_core::trace_warn!(
"[Retry {}/{}] Failed to download {}: {}. Retrying in {}ms...",
attempt, config.max_retries, url, e, delay
);
tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
}
last_error = Some(e);
}
}
}
Err(last_error.unwrap_or_else(|| InstallerError::RetriesExhausted {
attempts: config.max_retries,
url: url.to_string(),
}))
}
async fn download_large_file_once(
url: &str,
dest: &Path,
sha1: Option<&str>,
#[cfg(feature = "events")] event_bus: Option<&EventBus>,
) -> InstallerResult<()> {
let downloaded = fs::metadata(dest).await.map(|meta| meta.len()).unwrap_or(0);
let mut request = CLIENT.get(url);
if downloaded > 0 {
request = request.header(RANGE, format!("bytes={}-", downloaded));
}
let response = request.send().await?;
let status = response.status();
if status == StatusCode::RANGE_NOT_SATISFIABLE {
let _ = fs::remove_file(dest).await;
return Err(InstallerError::StalePartialFile {
url: url.to_string(),
});
}
if !status.is_success() {
return Err(InstallerError::HttpStatus {
status: status.as_u16(),
url: url.to_string(),
});
}
if let Some(parent) = dest.parent() {
mkdir!(parent);
}
let mut hasher = Sha1::new();
let file = if status == StatusCode::PARTIAL_CONTENT {
seed_hasher(dest, &mut hasher).await?;
OpenOptions::new().append(true).open(dest).await?
} else {
fs::File::create(dest).await?
};
let mut writer = BufWriter::with_capacity(256 * 1024, file);
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
writer.write_all(&chunk).await?;
hasher.update(&chunk);
#[cfg(feature = "events")]
if let Some(bus) = event_bus {
bus.emit(Event::Launch(LaunchEvent::InstallProgress {
bytes: chunk.len() as u64,
}));
}
}
writer.flush().await?;
if let Some(expected) = sha1 {
let digest = hex::encode(hasher.finalize());
if !digest.eq_ignore_ascii_case(expected) {
let _ = fs::remove_file(dest).await;
return Err(InstallerError::Sha1Mismatch {
url: url.to_string(),
expected: expected.to_string(),
actual: digest,
});
}
}
Ok(())
}
async fn seed_hasher(dest: &Path, hasher: &mut Sha1) -> InstallerResult<()> {
let mut partial = fs::File::open(dest).await?;
let mut buffer = vec![0u8; 256 * 1024];
loop {
let read = partial.read(&mut buffer).await?;
if read == 0 {
break;
}
hasher.update(&buffer[..read]);
}
Ok(())
}
pub async fn download_with_concurrency_limit(
tasks: Vec<DownloadTask<'_>>,
#[cfg(feature = "events")] event_bus: Option<&EventBus>,
) -> InstallerResult<()> {
let config = get_config();
let semaphore = Arc::new(Semaphore::new(config.max_concurrent_downloads));
let futures: Vec<_> = tasks
.iter()
.map(|task| {
let sem = semaphore.clone();
async move {
let _permit = sem.acquire().await
.map_err(|_| InstallerError::ConcurrencyClosed)?;
download_large_file(
task.url,
&task.dest,
task.sha1,
#[cfg(feature = "events")]
event_bus,
)
.await
}
})
.collect();
try_join_all(futures).await?;
Ok(())
}
pub async fn download_small_with_concurrency_limit(
tasks: Vec<DownloadTask<'_>>,
#[cfg(feature = "events")] event_bus: Option<&EventBus>,
) -> InstallerResult<()> {
let config = get_config();
let semaphore = Arc::new(Semaphore::new(config.max_concurrent_downloads));
let futures: Vec<_> = tasks
.iter()
.map(|task| {
let sem = semaphore.clone();
async move {
let _permit = sem.acquire().await
.map_err(|_| InstallerError::ConcurrencyClosed)?;
download_small_file(
task.url,
&task.dest,
task.sha1,
#[cfg(feature = "events")]
event_bus,
)
.await
}
})
.collect();
try_join_all(futures).await?;
Ok(())
}