use std::path::Path;
use std::time::{Duration, Instant};
use futures::StreamExt;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use crate::error::FetcherError;
use crate::{FetcherPhase, FetcherProgress};
const PROGRESS_BYTES_INTERVAL: u64 = 100 * 1024;
const PROGRESS_TIME_INTERVAL: Duration = Duration::from_millis(100);
pub(crate) async fn download(
url: &str,
dest_path: &Path,
progress_cb: Option<&(dyn Fn(FetcherProgress) + Send + Sync)>,
) -> Result<(), FetcherError> {
let resp = reqwest::get(url).await?.error_for_status()?;
let total = resp.content_length();
let mut file = File::create(dest_path).await?;
let mut stream = resp.bytes_stream();
let mut downloaded: u64 = 0;
let mut last_emit_bytes: u64 = 0;
let mut last_emit_time = Instant::now();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
file.write_all(&chunk).await?;
downloaded += chunk.len() as u64;
if let Some(cb) = progress_cb {
let bytes_since = downloaded - last_emit_bytes;
let time_since = last_emit_time.elapsed();
if bytes_since >= PROGRESS_BYTES_INTERVAL || time_since >= PROGRESS_TIME_INTERVAL {
cb(FetcherProgress {
downloaded,
total,
phase: FetcherPhase::Downloading,
});
last_emit_bytes = downloaded;
last_emit_time = Instant::now();
}
}
}
file.flush().await?;
file.sync_all().await?;
if let Some(cb) = progress_cb {
cb(FetcherProgress {
downloaded,
total,
phase: FetcherPhase::Done,
});
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn downloads_file_and_invokes_progress_callback() {
let server = MockServer::start().await;
let body = b"hello chrome zip payload".to_vec();
Mock::given(method("GET"))
.and(path("/chrome.zip"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Length", body.len().to_string().as_str())
.set_body_bytes(body.clone()),
)
.mount(&server)
.await;
let tmp = tempfile::NamedTempFile::new().unwrap();
let dest = tmp.path().to_path_buf();
let url = format!("{}/chrome.zip", server.uri());
let calls = Arc::new(AtomicUsize::new(0));
let calls_cb = Arc::clone(&calls);
let cb = move |_p: FetcherProgress| {
calls_cb.fetch_add(1, Ordering::SeqCst);
};
download(&url, &dest, Some(&cb)).await.unwrap();
let written = tokio::fs::read(&dest).await.unwrap();
assert_eq!(written, body);
assert!(
calls.load(Ordering::SeqCst) >= 1,
"progress callback should fire at least once (got {})",
calls.load(Ordering::SeqCst)
);
}
}