use url::Url;
use crate::backend::Backend;
#[derive(Debug)]
pub struct DownloadResponse {
pub file_path: std::path::PathBuf,
pub bytes_downloaded: u64,
}
pub struct DownloadBuilder {
backend: Backend,
url: Url,
file_path: Option<std::path::PathBuf>,
progress_callback: Option<Box<dyn Fn(u64, Option<u64>) + Send + Sync + 'static>>,
}
impl DownloadBuilder {
pub(crate) fn new(backend: Backend, url: Url) -> Self {
Self {
backend,
url,
file_path: None,
progress_callback: None,
}
}
pub fn to_file<P: AsRef<std::path::Path>>(mut self, path: P) -> Self {
self.file_path = Some(path.as_ref().to_path_buf());
self
}
pub fn progress<F>(mut self, callback: F) -> Self
where
F: Fn(u64, Option<u64>) + Send + Sync + 'static,
{
self.progress_callback = Some(Box::new(callback));
self
}
pub async fn send(self) -> crate::Result<DownloadResponse> {
let file_path = self.file_path.ok_or_else(|| {
crate::Error::Internal("Download file path not specified".to_string())
})?;
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
crate::Error::Internal(format!("Failed to create parent directory: {}", e))
})?;
}
let request_builder = crate::RequestBuilder::new(http::Method::GET, self.url, self.backend);
let response = request_builder.send().await?;
let total_bytes = response
.headers()
.get("content-length")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok());
let mut file = tokio::fs::File::create(&file_path)
.await
.map_err(|e| crate::Error::Internal(format!("Failed to create file: {}", e)))?;
let mut stream = response.stream();
let mut bytes_downloaded = 0u64;
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result?;
bytes_downloaded += chunk.len() as u64;
tokio::io::AsyncWriteExt::write_all(&mut file, &chunk)
.await
.map_err(|e| crate::Error::Internal(format!("Failed to write to file: {}", e)))?;
if let Some(ref callback) = self.progress_callback {
callback(bytes_downloaded, total_bytes);
}
}
tokio::io::AsyncWriteExt::flush(&mut file)
.await
.map_err(|e| crate::Error::Internal(format!("Failed to flush file: {}", e)))?;
Ok(DownloadResponse {
file_path,
bytes_downloaded,
})
}
}