use std::path::Path;
use anyhow::{Context, Result};
use futures::StreamExt;
use tokio::io::AsyncWriteExt;
use tracing::{debug, warn};
use crate::traits::{DownloadHandle, ProgressCallback, VerifiedFile};
pub const MAX_RETRIES: u32 = 3;
pub const BACKOFF_BASE_MS: u64 = 1000;
pub async fn stream_to_file(
resp: reqwest::Response,
dest: &Path,
total_hint: u64,
progress: &ProgressCallback,
) -> Result<u64> {
let total = resp.content_length().unwrap_or(total_hint);
let mut file = tokio::fs::File::create(dest).await?;
let mut downloaded: u64 = 0;
let mut stream = resp.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
downloaded += chunk.len() as u64;
progress(downloaded, total);
}
file.flush().await?;
Ok(downloaded)
}
pub async fn verify_and_wrap(dest: &Path, expected_hash: u64) -> Result<VerifiedFile> {
modde_core::hash::verify_xxhash_compat(dest, expected_hash)
.await
.context("hash verification failed (tried xxh64 and xxh3)")?;
Ok(VerifiedFile { path: dest.to_path_buf(), hash: expected_hash })
}
pub async fn ensure_parent(dest: &Path) -> Result<()> {
if let Some(parent) = dest.parent() {
tokio::fs::create_dir_all(parent).await?;
}
Ok(())
}
pub async fn with_retry<F, Fut, T>(label: &str, f: F) -> Result<T>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
{
for attempt in 0..MAX_RETRIES {
match f().await {
Ok(val) => return Ok(val),
Err(e) => {
if attempt + 1 < MAX_RETRIES {
let base_delay = BACKOFF_BASE_MS * (1 << attempt);
let jitter = base_delay / 4 + (base_delay / 2).wrapping_mul(attempt as u64 + 1) % (base_delay / 2 + 1);
let delay = base_delay + jitter;
warn!(attempt = attempt + 1, delay_ms = delay, error = %e, "{label} failed, retrying");
tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
} else {
return Err(e).context(format!("{label} failed after {MAX_RETRIES} attempts"));
}
}
}
}
unreachable!()
}
pub async fn simple_download(
client: &reqwest::Client,
handle: &DownloadHandle,
dest: &Path,
progress: &ProgressCallback,
) -> Result<VerifiedFile> {
ensure_parent(dest).await?;
let resp = client
.get(&handle.url)
.send()
.await?
.error_for_status()?;
let downloaded = stream_to_file(resp, dest, handle.size_hint.unwrap_or(0), progress).await?;
debug!(bytes = downloaded, "download complete");
verify_and_wrap(dest, handle.expected_hash).await
}