use anyhow::{anyhow, Context, Result};
use log::{debug, info, warn};
use std::fs::{self, File};
use std::io::copy;
use std::path::Path;
use tokio::runtime::Runtime;
pub struct TurboDownloader {
runtime: Runtime,
client: turbo_cdn::TurboCdn,
}
impl TurboDownloader {
pub fn new() -> Result<Self> {
let runtime = Runtime::new().with_context(|| "Failed to create Tokio runtime")?;
let client = runtime
.block_on(async { turbo_cdn::TurboCdn::new().await })
.with_context(|| "Failed to create TurboCdn client")?;
Ok(Self { runtime, client })
}
pub fn download_file(&self, url: &str, dest: &Path) -> Result<()> {
info!("Downloading {} to {}", url, dest.display());
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {}", parent.display()))?;
}
let result = self
.runtime
.block_on(async { self.client.download_smart_to_path(url, dest).await })
.with_context(|| format!("Failed to download from {url}"))?;
info!(
"Downloaded {} bytes to {} at {:.2} MB/s",
result.size,
dest.display(),
result.speed / 1024.0 / 1024.0
);
Ok(())
}
pub fn get_optimized_url(&self, url: &str) -> Result<String> {
debug!("Getting optimized URL for: {url}");
let optimized_url = self
.runtime
.block_on(async { self.client.get_optimal_url(url).await })
.with_context(|| format!("Failed to get optimized URL for {url}"))?;
debug!("Optimized URL: {optimized_url}");
Ok(optimized_url)
}
pub fn download_with_progress<F>(
&self,
url: &str,
dest: &Path,
progress_callback: F,
) -> Result<()>
where
F: Fn(f64) + Send + 'static,
{
info!(
"Downloading {} to {} with progress tracking",
url,
dest.display()
);
progress_callback(0.0);
let result = self.download_file(url, dest);
progress_callback(100.0);
result
}
}
pub fn fallback_download_file(url: &str, dest: &Path) -> Result<()> {
warn!("Using fallback download method for {url}");
let runtime = Runtime::new().with_context(|| "Failed to create Tokio runtime")?;
runtime.block_on(async {
let client = reqwest::Client::new();
let response = client
.get(url)
.send()
.await
.with_context(|| format!("Failed to download from {url}"))?;
if !response.status().is_success() {
return Err(anyhow!(
"Failed to download from {}: {}",
url,
response.status()
));
}
let mut file = File::create(dest)
.with_context(|| format!("Failed to create file: {}", dest.display()))?;
let mut content = std::io::Cursor::new(
response
.bytes()
.await
.with_context(|| "Failed to read response body")?,
);
copy(&mut content, &mut file)
.with_context(|| format!("Failed to write to file: {}", dest.display()))?;
Ok(())
})
}
pub fn smart_download_file(url: &str, dest: &Path) -> Result<()> {
match TurboDownloader::new() {
Ok(downloader) => match downloader.download_file(url, dest) {
Ok(()) => {
debug!("Successfully downloaded using turbo-cdn");
return Ok(());
}
Err(e) => {
warn!("Turbo-cdn download failed: {e}, falling back to reqwest");
}
},
Err(e) => {
warn!("Failed to create turbo downloader: {e}, using fallback");
}
}
fallback_download_file(url, dest)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_turbo_downloader_creation() {
let result = TurboDownloader::new();
assert!(result.is_ok(), "Should be able to create TurboDownloader");
}
#[test]
fn test_smart_download_fallback() {
let temp_dir = TempDir::new().unwrap();
let dest = temp_dir.path().join("test_file.txt");
let result = smart_download_file("https://httpbin.org/get", &dest);
println!("Smart download result: {result:?}");
}
#[test]
fn test_get_optimized_url() {
let downloader = TurboDownloader::new().unwrap();
let test_url =
"https://github.com/astral-sh/uv/releases/download/0.7.6/uv-x86_64-pc-windows-msvc.zip";
match downloader.get_optimized_url(test_url) {
Ok(optimized) => {
println!("Original: {test_url}");
println!("Optimized: {optimized}");
assert!(!optimized.is_empty());
}
Err(e) => {
println!("Failed to get optimized URL (expected in CI): {e}");
}
}
}
}