turbo_cdn/
lib.rs

1// Licensed under the MIT License
2// Copyright (c) 2025 Hal <hal.long@outlook.com>
3
4//! # Turbo CDN
5//!
6//! Revolutionary global download accelerator for open-source software with AI optimization,
7//! multi-CDN routing, and P2P acceleration.
8//!
9//! ## Features
10//!
11//! - **Multi-CDN Support**: GitHub Releases, jsDelivr, Fastly, Cloudflare
12//! - **Intelligent Routing**: AI-powered CDN selection and failover
13//! - **Parallel Downloads**: Chunked downloads with automatic optimization
14//! - **Compliance First**: Built-in verification for open-source content only
15//! - **Caching**: Smart caching with compression and TTL management
16//! - **Progress Tracking**: Real-time download progress with callbacks
17//!
18//! ## Quick Start
19//!
20//! ```rust,no_run
21//! use turbo_cdn::*;
22//!
23//! #[tokio::main]
24//! async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
25//!     let mut downloader = TurboCdn::builder()
26//!         .with_sources(&[
27//!             Source::github(),
28//!             Source::jsdelivr(),
29//!             Source::fastly(),
30//!         ])
31//!         .with_region(Region::Global)
32//!         .build()
33//!         .await?;
34//!
35//!     let options = DownloadOptions {
36//!         progress_callback: Some(Box::new(|progress| {
37//!             println!("Downloaded: {:.1}%", progress.percentage);
38//!         })),
39//!         ..Default::default()
40//!     };
41//!
42//!     // Note: This would perform the actual download in a real implementation
43//!     // let result = downloader.download("oven-sh/bun", "v1.0.0", "bun-linux-x64.zip", options).await?;
44//!     // println!("Downloaded to: {}", result.path.display());
45//!     Ok(())
46//! }
47//! ```
48
49pub mod cache;
50pub mod compliance;
51pub mod config;
52pub mod domain_manager;
53pub mod downloader;
54pub mod error;
55pub mod geo_detection;
56pub mod progress;
57pub mod router;
58pub mod sources;
59pub mod utils;
60
61use std::path::PathBuf;
62use tracing::{info, warn};
63
64// Re-export commonly used types
65pub use cache::{CacheManager, CacheStats};
66pub use compliance::{ComplianceChecker, ComplianceResult};
67pub use config::{Region, TurboCdnConfig};
68pub use downloader::{DownloadOptions, DownloadResult, Downloader};
69pub use error::{Result, TurboCdnError};
70pub use progress::{ConsoleProgressReporter, ProgressCallback, ProgressInfo, ProgressTracker};
71pub use router::{RoutingDecision, SmartRouter};
72pub use sources::{
73    cloudflare::CloudflareSource, fastly::FastlySource, github::GitHubSource,
74    jsdelivr::JsDelivrSource, DownloadUrl, SourceManager,
75};
76pub use utils::PathManager;
77
78/// Parsed URL information from various sources
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct ParsedUrl {
81    /// Repository in format "owner/repo"
82    pub repository: String,
83    /// Version/tag name
84    pub version: String,
85    /// Filename
86    pub filename: String,
87    /// Original URL
88    pub original_url: String,
89    /// Detected source type
90    pub source_type: DetectedSourceType,
91}
92
93/// Detected source type from URL parsing
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum DetectedSourceType {
96    /// GitHub releases
97    GitHub,
98    /// jsDelivr CDN
99    JsDelivr,
100    /// Fastly CDN (jsDelivr)
101    Fastly,
102    /// Cloudflare CDN
103    Cloudflare,
104    /// npm registry
105    Npm,
106    /// Python Package Index (PyPI)
107    PyPI,
108    /// Golang module proxy
109    GoProxy,
110    /// Rust crates.io
111    CratesIo,
112    /// Maven Central Repository
113    Maven,
114    /// NuGet Gallery
115    NuGet,
116    /// Docker Hub
117    DockerHub,
118    /// GitLab releases
119    GitLab,
120    /// Bitbucket downloads
121    Bitbucket,
122    /// SourceForge files
123    SourceForge,
124    /// Other/unknown source
125    Other(String),
126}
127
128/// Main TurboCdn client
129#[derive(Debug)]
130pub struct TurboCdn {
131    downloader: Downloader,
132}
133
134/// Builder for TurboCdn client
135#[derive(Debug)]
136pub struct TurboCdnBuilder {
137    config: TurboCdnConfig,
138    sources: Vec<Source>,
139}
140
141/// Download source types
142#[derive(Debug, Clone)]
143pub enum Source {
144    GitHub,
145    JsDelivr,
146    Fastly,
147    Cloudflare,
148}
149
150impl TurboCdn {
151    /// Create a new TurboCdn builder
152    pub fn builder() -> TurboCdnBuilder {
153        TurboCdnBuilder::new()
154    }
155
156    /// Create a TurboCdn client with default configuration
157    pub async fn new() -> Result<Self> {
158        Self::builder().build().await
159    }
160
161    /// Download a file from a repository
162    pub async fn download(
163        &mut self,
164        repository: &str,
165        version: &str,
166        file_name: &str,
167        options: DownloadOptions,
168    ) -> Result<DownloadResult> {
169        self.downloader
170            .download(repository, version, file_name, options)
171            .await
172    }
173
174    /// Download from any supported URL with automatic CDN optimization
175    ///
176    /// This method accepts URLs from various sources and automatically optimizes them:
177    ///
178    /// **Supported URL formats:**
179    /// - GitHub: `https://github.com/owner/repo/releases/download/tag/file.zip`
180    /// - GitLab: `https://gitlab.com/owner/repo/-/releases/tag/downloads/file.zip`
181    /// - Bitbucket: `https://bitbucket.org/owner/repo/downloads/file.zip`
182    /// - jsDelivr: `https://cdn.jsdelivr.net/gh/owner/repo@tag/file.zip`
183    /// - Fastly: `https://fastly.jsdelivr.net/gh/owner/repo@tag/file.zip`
184    /// - Cloudflare: `https://cdnjs.cloudflare.com/ajax/libs/library/version/file.js`
185    /// - npm: `https://registry.npmjs.org/package/-/package-version.tgz`
186    /// - PyPI: `https://files.pythonhosted.org/packages/source/p/package/package-version.tar.gz`
187    /// - Go Proxy: `https://proxy.golang.org/module/@v/version.zip`
188    /// - Crates.io: `https://crates.io/api/v1/crates/crate/version/download`
189    /// - Maven: `https://repo1.maven.org/maven2/group/artifact/version/artifact-version.jar`
190    /// - NuGet: `https://api.nuget.org/v3-flatcontainer/package/version/package.version.nupkg`
191    /// - Docker Hub: `https://registry-1.docker.io/v2/library/image/manifests/tag`
192    /// - SourceForge: `https://downloads.sourceforge.net/project/name/file.zip`
193    ///
194    /// **Automatic optimization:**
195    /// 1. Parses the URL to extract repository, version, and filename
196    /// 2. Detects user's geographic location
197    /// 3. Selects the optimal CDN based on location and performance
198    /// 4. Downloads using the best available source with failover
199    ///
200    /// # Arguments
201    /// * `url` - The source URL from any supported CDN or repository
202    /// * `options` - Download options (optional, uses defaults if None)
203    ///
204    /// # Example
205    /// ```rust,no_run
206    /// use turbo_cdn::*;
207    ///
208    /// #[tokio::main]
209    /// async fn main() -> turbo_cdn::Result<()> {
210    ///     let mut downloader = TurboCdn::new().await?;
211    ///
212    ///     // GitHub releases URL
213    ///     let result = downloader.download_from_url(
214    ///         "https://github.com/oven-sh/bun/releases/download/bun-v1.2.9/bun-bun-v1.2.9.zip",
215    ///         None
216    ///     ).await?;
217    ///
218    ///     // jsDelivr URL
219    ///     let result = downloader.download_from_url(
220    ///         "https://cdn.jsdelivr.net/gh/microsoft/vscode@1.74.0/package.json",
221    ///         None
222    ///     ).await?;
223    ///
224    ///     println!("Downloaded to: {}", result.path.display());
225    ///     Ok(())
226    /// }
227    /// ```
228    pub async fn download_from_url(
229        &mut self,
230        url: &str,
231        options: Option<DownloadOptions>,
232    ) -> Result<DownloadResult> {
233        let parsed_url = self.parse_url(url)?;
234        let options = options.unwrap_or_default();
235
236        self.download(
237            &parsed_url.repository,
238            &parsed_url.version,
239            &parsed_url.filename,
240            options,
241        )
242        .await
243    }
244
245    /// Get the optimal CDN URL for a given source URL without downloading
246    ///
247    /// This method parses the input URL and returns the best CDN URL based on:
248    /// 1. User's geographic location (detected automatically)
249    /// 2. CDN performance metrics
250    /// 3. Source availability and reliability
251    ///
252    /// # Arguments
253    /// * `url` - The source URL from any supported CDN or repository
254    ///
255    /// # Returns
256    /// * `String` containing the optimal CDN URL for the user's location
257    ///
258    /// # Example
259    /// ```rust,no_run
260    /// use turbo_cdn::*;
261    ///
262    /// #[tokio::main]
263    /// async fn main() -> turbo_cdn::Result<()> {
264    ///     let downloader = TurboCdn::new().await?;
265    ///
266    ///     let optimal_url = downloader.get_optimal_url(
267    ///         "https://github.com/oven-sh/bun/releases/download/bun-v1.2.9/bun-bun-v1.2.9.zip"
268    ///     ).await?;
269    ///
270    ///     println!("Optimal URL: {}", optimal_url);
271    ///     // Might output: https://fastly.jsdelivr.net/gh/oven-sh/bun@bun-v1.2.9/bun-bun-v1.2.9.zip
272    ///     Ok(())
273    /// }
274    /// ```
275    pub async fn get_optimal_url(&self, url: &str) -> Result<String> {
276        let parsed_url = self.parse_url(url)?;
277
278        // Use the downloader's method to get optimal URL
279        self.downloader
280            .get_optimal_url(
281                &parsed_url.repository,
282                &parsed_url.version,
283                &parsed_url.filename,
284            )
285            .await
286    }
287
288    /// Get repository metadata
289    pub async fn get_repository_metadata(
290        &self,
291        repository: &str,
292    ) -> Result<sources::RepositoryMetadata> {
293        self.downloader.get_repository_metadata(repository).await
294    }
295
296    /// Parse any supported URL into components (public for testing)
297    ///
298    /// Supports URLs from various sources:
299    /// - GitHub: `https://github.com/{owner}/{repo}/releases/download/{tag}/{filename}`
300    /// - GitLab: `https://gitlab.com/{owner}/{repo}/-/releases/{tag}/downloads/{filename}`
301    /// - Bitbucket: `https://bitbucket.org/{owner}/{repo}/downloads/{filename}`
302    /// - jsDelivr: `https://cdn.jsdelivr.net/gh/{owner}/{repo}@{tag}/{filename}`
303    /// - Fastly: `https://fastly.jsdelivr.net/gh/{owner}/{repo}@{tag}/{filename}`
304    /// - Cloudflare: `https://cdnjs.cloudflare.com/ajax/libs/{library}/{version}/{filename}`
305    /// - npm: `https://registry.npmjs.org/{package}/-/{package}-{version}.tgz`
306    /// - PyPI: `https://files.pythonhosted.org/packages/source/{first_letter}/{package}/{package}-{version}.tar.gz`
307    /// - Go Proxy: `https://proxy.golang.org/{module}/@v/{version}.zip`
308    /// - Crates.io: `https://crates.io/api/v1/crates/{crate}/{version}/download`
309    /// - Maven: `https://repo1.maven.org/maven2/{group_path}/{artifact}/{version}/{artifact}-{version}.jar`
310    /// - NuGet: `https://api.nuget.org/v3-flatcontainer/{package}/{version}/{package}.{version}.nupkg`
311    /// - Docker Hub: `https://registry-1.docker.io/v2/library/{image}/manifests/{tag}`
312    /// - SourceForge: `https://downloads.sourceforge.net/project/{project}/{filename}`
313    ///
314    /// # Arguments
315    /// * `url` - The URL to parse
316    ///
317    /// # Returns
318    /// * `ParsedUrl` containing repository, version, filename, and detected source type
319    ///
320    /// # Errors
321    /// * Returns error if URL format is invalid or unsupported
322    pub fn parse_url(&self, url: &str) -> Result<ParsedUrl> {
323        let url_obj = url::Url::parse(url)
324            .map_err(|e| TurboCdnError::config(format!("Invalid URL format: {}", e)))?;
325
326        let host = url_obj
327            .host_str()
328            .ok_or_else(|| TurboCdnError::config("URL must have a valid host".to_string()))?;
329
330        match host {
331            "github.com" => self.parse_github_url(&url_obj, url),
332            "gitlab.com" => self.parse_gitlab_url(&url_obj, url),
333            "bitbucket.org" => self.parse_bitbucket_url(&url_obj, url),
334            "cdn.jsdelivr.net" => self.parse_jsdelivr_url(&url_obj, url),
335            "fastly.jsdelivr.net" => self.parse_fastly_url(&url_obj, url),
336            "cdnjs.cloudflare.com" => self.parse_cloudflare_url(&url_obj, url),
337            "registry.npmjs.org" => self.parse_npm_url(&url_obj, url),
338            "files.pythonhosted.org" => self.parse_pypi_url(&url_obj, url),
339            "proxy.golang.org" => self.parse_go_proxy_url(&url_obj, url),
340            "crates.io" => self.parse_crates_io_url(&url_obj, url),
341            "repo1.maven.org" => self.parse_maven_url(&url_obj, url),
342            "api.nuget.org" => self.parse_nuget_url(&url_obj, url),
343            "registry-1.docker.io" => self.parse_docker_hub_url(&url_obj, url),
344            "downloads.sourceforge.net" => self.parse_sourceforge_url(&url_obj, url),
345            _ => Err(TurboCdnError::config(format!(
346                "Unsupported URL host: {}. Supported hosts: github.com, gitlab.com, bitbucket.org, cdn.jsdelivr.net, fastly.jsdelivr.net, cdnjs.cloudflare.com, registry.npmjs.org, files.pythonhosted.org, proxy.golang.org, crates.io, repo1.maven.org, api.nuget.org, registry-1.docker.io, downloads.sourceforge.net",
347                host
348            ))),
349        }
350    }
351
352    /// Parse GitHub releases URL
353    fn parse_github_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
354        let path = url_obj.path();
355        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
356
357        // Expected format: {owner}/{repo}/releases/download/{tag}/{filename}
358        if path_segments.len() < 6 {
359            return Err(TurboCdnError::config(
360                "Invalid GitHub releases URL format. Expected: https://github.com/{owner}/{repo}/releases/download/{tag}/{filename}".to_string(),
361            ));
362        }
363
364        if path_segments[2] != "releases" || path_segments[3] != "download" {
365            return Err(TurboCdnError::config(
366                "URL must be a GitHub releases download URL".to_string(),
367            ));
368        }
369
370        let owner = path_segments[0];
371        let repo = path_segments[1];
372        let tag = path_segments[4];
373        let filename = path_segments[5..].join("/");
374
375        self.validate_components(owner, repo, tag, &filename)?;
376
377        Ok(ParsedUrl {
378            repository: format!("{}/{}", owner, repo),
379            version: tag.to_string(),
380            filename,
381            original_url: original_url.to_string(),
382            source_type: DetectedSourceType::GitHub,
383        })
384    }
385
386    /// Parse GitLab releases URL
387    fn parse_gitlab_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
388        let path = url_obj.path();
389        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
390
391        // Expected format: {owner}/{repo}/-/releases/{tag}/downloads/{filename}
392        if path_segments.len() < 7 {
393            return Err(TurboCdnError::config(
394                "Invalid GitLab releases URL format. Expected: https://gitlab.com/{owner}/{repo}/-/releases/{tag}/downloads/{filename}".to_string(),
395            ));
396        }
397
398        if path_segments[2] != "-"
399            || path_segments[3] != "releases"
400            || path_segments[5] != "downloads"
401        {
402            return Err(TurboCdnError::config(
403                "URL must be a GitLab releases download URL".to_string(),
404            ));
405        }
406
407        let owner = path_segments[0];
408        let repo = path_segments[1];
409        let tag = path_segments[4];
410        let filename = path_segments[6..].join("/");
411
412        self.validate_components(owner, repo, tag, &filename)?;
413
414        Ok(ParsedUrl {
415            repository: format!("{}/{}", owner, repo),
416            version: tag.to_string(),
417            filename,
418            original_url: original_url.to_string(),
419            source_type: DetectedSourceType::GitLab,
420        })
421    }
422
423    /// Parse Bitbucket downloads URL
424    fn parse_bitbucket_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
425        let path = url_obj.path();
426        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
427
428        // Expected format: {owner}/{repo}/downloads/{filename}
429        if path_segments.len() < 4 {
430            return Err(TurboCdnError::config(
431                "Invalid Bitbucket downloads URL format. Expected: https://bitbucket.org/{owner}/{repo}/downloads/{filename}".to_string(),
432            ));
433        }
434
435        if path_segments[2] != "downloads" {
436            return Err(TurboCdnError::config(
437                "URL must be a Bitbucket downloads URL".to_string(),
438            ));
439        }
440
441        let owner = path_segments[0];
442        let repo = path_segments[1];
443        let filename = path_segments[3..].join("/");
444
445        // For Bitbucket, we'll extract version from filename if possible
446        let version = self
447            .extract_version_from_filename(&filename)
448            .unwrap_or_else(|| "latest".to_string());
449
450        self.validate_components(owner, repo, &version, &filename)?;
451
452        Ok(ParsedUrl {
453            repository: format!("{}/{}", owner, repo),
454            version,
455            filename,
456            original_url: original_url.to_string(),
457            source_type: DetectedSourceType::Bitbucket,
458        })
459    }
460
461    /// Parse jsDelivr CDN URL
462    fn parse_jsdelivr_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
463        let path = url_obj.path();
464        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
465
466        // Expected format: gh/{owner}/{repo}@{tag}/{filename}
467        if path_segments.len() < 4 || path_segments[0] != "gh" {
468            return Err(TurboCdnError::config(
469                "Invalid jsDelivr URL format. Expected: https://cdn.jsdelivr.net/gh/{owner}/{repo}@{tag}/{filename}".to_string(),
470            ));
471        }
472
473        let owner = path_segments[1];
474        let repo_and_tag = path_segments[2];
475        let filename = path_segments[3..].join("/");
476
477        // Parse repo@tag format
478        let (repo, tag) = if let Some(at_pos) = repo_and_tag.find('@') {
479            let repo = &repo_and_tag[..at_pos];
480            let tag = &repo_and_tag[at_pos + 1..];
481            (repo, tag)
482        } else {
483            return Err(TurboCdnError::config(
484                "Invalid jsDelivr URL: missing @tag in repository specification".to_string(),
485            ));
486        };
487
488        self.validate_components(owner, repo, tag, &filename)?;
489
490        Ok(ParsedUrl {
491            repository: format!("{}/{}", owner, repo),
492            version: tag.to_string(),
493            filename,
494            original_url: original_url.to_string(),
495            source_type: DetectedSourceType::JsDelivr,
496        })
497    }
498
499    /// Parse Fastly CDN URL (same format as jsDelivr)
500    fn parse_fastly_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
501        let path = url_obj.path();
502        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
503
504        // Expected format: gh/{owner}/{repo}@{tag}/{filename}
505        if path_segments.len() < 4 || path_segments[0] != "gh" {
506            return Err(TurboCdnError::config(
507                "Invalid Fastly URL format. Expected: https://fastly.jsdelivr.net/gh/{owner}/{repo}@{tag}/{filename}".to_string(),
508            ));
509        }
510
511        let owner = path_segments[1];
512        let repo_and_tag = path_segments[2];
513        let filename = path_segments[3..].join("/");
514
515        // Parse repo@tag format
516        let (repo, tag) = if let Some(at_pos) = repo_and_tag.find('@') {
517            let repo = &repo_and_tag[..at_pos];
518            let tag = &repo_and_tag[at_pos + 1..];
519            (repo, tag)
520        } else {
521            return Err(TurboCdnError::config(
522                "Invalid Fastly URL: missing @tag in repository specification".to_string(),
523            ));
524        };
525
526        self.validate_components(owner, repo, tag, &filename)?;
527
528        Ok(ParsedUrl {
529            repository: format!("{}/{}", owner, repo),
530            version: tag.to_string(),
531            filename,
532            original_url: original_url.to_string(),
533            source_type: DetectedSourceType::Fastly,
534        })
535    }
536
537    /// Parse Cloudflare CDN URL
538    fn parse_cloudflare_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
539        let path = url_obj.path();
540        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
541
542        // Expected format: ajax/libs/{library}/{version}/{filename}
543        if path_segments.len() < 5 || path_segments[0] != "ajax" || path_segments[1] != "libs" {
544            return Err(TurboCdnError::config(
545                "Invalid Cloudflare URL format. Expected: https://cdnjs.cloudflare.com/ajax/libs/{library}/{version}/{filename}".to_string(),
546            ));
547        }
548
549        let library = path_segments[2];
550        let version = path_segments[3];
551        let filename = path_segments[4..].join("/");
552
553        if library.is_empty() || version.is_empty() || filename.is_empty() {
554            return Err(TurboCdnError::config(
555                "Invalid Cloudflare URL: missing required components".to_string(),
556            ));
557        }
558
559        Ok(ParsedUrl {
560            repository: format!("cdnjs/{}", library), // Use cdnjs as pseudo-owner
561            version: version.to_string(),
562            filename,
563            original_url: original_url.to_string(),
564            source_type: DetectedSourceType::Cloudflare,
565        })
566    }
567
568    /// Parse npm registry URL
569    fn parse_npm_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
570        let path = url_obj.path();
571        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
572
573        // Expected format: {package}/-/{package}-{version}.tgz
574        if path_segments.len() < 3 || path_segments[1] != "-" {
575            return Err(TurboCdnError::config(
576                "Invalid npm URL format. Expected: https://registry.npmjs.org/{package}/-/{package}-{version}.tgz".to_string(),
577            ));
578        }
579
580        let package = path_segments[0];
581        let filename = path_segments[2];
582
583        // Extract version from filename (package-version.tgz)
584        let version = if let Some(tgz_pos) = filename.rfind(".tgz") {
585            let without_ext = &filename[..tgz_pos];
586            if let Some(dash_pos) = without_ext.rfind('-') {
587                &without_ext[dash_pos + 1..]
588            } else {
589                return Err(TurboCdnError::config(
590                    "Invalid npm filename: cannot extract version".to_string(),
591                ));
592            }
593        } else {
594            return Err(TurboCdnError::config(
595                "Invalid npm filename: must end with .tgz".to_string(),
596            ));
597        };
598
599        if package.is_empty() || version.is_empty() {
600            return Err(TurboCdnError::config(
601                "Invalid npm URL: missing required components".to_string(),
602            ));
603        }
604
605        Ok(ParsedUrl {
606            repository: format!("npm/{}", package), // Use npm as pseudo-owner
607            version: version.to_string(),
608            filename: filename.to_string(),
609            original_url: original_url.to_string(),
610            source_type: DetectedSourceType::Npm,
611        })
612    }
613
614    /// Validate URL components
615    fn validate_components(
616        &self,
617        owner: &str,
618        repo: &str,
619        tag: &str,
620        filename: &str,
621    ) -> Result<()> {
622        if owner.is_empty() || repo.is_empty() || tag.is_empty() || filename.is_empty() {
623            return Err(TurboCdnError::config(
624                "Invalid URL: missing required components (owner, repo, tag, or filename)"
625                    .to_string(),
626            ));
627        }
628        Ok(())
629    }
630
631    /// Extract version from filename using common patterns (public for testing)
632    pub fn extract_version_from_filename(&self, filename: &str) -> Option<String> {
633        // Common version patterns in filenames
634        let patterns = [
635            r"v?(\d+\.\d+\.\d+)",   // v1.2.3 or 1.2.3
636            r"v?(\d+\.\d+)",        // v1.2 or 1.2
637            r"(\d{4}-\d{2}-\d{2})", // 2023-12-01
638            r"(\d{8})",             // 20231201
639        ];
640
641        for pattern in &patterns {
642            if let Ok(re) = regex::Regex::new(pattern) {
643                if let Some(captures) = re.captures(filename) {
644                    if let Some(version) = captures.get(1) {
645                        return Some(version.as_str().to_string());
646                    }
647                }
648            }
649        }
650
651        None
652    }
653
654    /// Parse PyPI URL
655    fn parse_pypi_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
656        let path = url_obj.path();
657        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
658
659        // Expected format: packages/source/{first_letter}/{package}/{package}-{version}.tar.gz
660        if path_segments.len() < 5 || path_segments[0] != "packages" || path_segments[1] != "source"
661        {
662            return Err(TurboCdnError::config(
663                "Invalid PyPI URL format. Expected: https://files.pythonhosted.org/packages/source/{first_letter}/{package}/{package}-{version}.tar.gz".to_string(),
664            ));
665        }
666
667        let package = path_segments[3];
668        let filename = path_segments[4];
669
670        // Extract version from filename
671        let version = if let Some(tar_pos) = filename.rfind(".tar.gz") {
672            let without_ext = &filename[..tar_pos];
673            if let Some(dash_pos) = without_ext.rfind('-') {
674                &without_ext[dash_pos + 1..]
675            } else {
676                return Err(TurboCdnError::config(
677                    "Invalid PyPI filename: cannot extract version".to_string(),
678                ));
679            }
680        } else {
681            return Err(TurboCdnError::config(
682                "Invalid PyPI filename: must end with .tar.gz".to_string(),
683            ));
684        };
685
686        Ok(ParsedUrl {
687            repository: format!("pypi/{}", package),
688            version: version.to_string(),
689            filename: filename.to_string(),
690            original_url: original_url.to_string(),
691            source_type: DetectedSourceType::PyPI,
692        })
693    }
694
695    /// Parse Go proxy URL
696    fn parse_go_proxy_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
697        let path = url_obj.path();
698        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
699
700        // Expected format: {module}/@v/{version}.zip
701        if path_segments.len() < 3 || !path_segments[path_segments.len() - 2].starts_with("@v") {
702            return Err(TurboCdnError::config(
703                "Invalid Go proxy URL format. Expected: https://proxy.golang.org/{module}/@v/{version}.zip".to_string(),
704            ));
705        }
706
707        let module_parts = &path_segments[..path_segments.len() - 2];
708        let module = module_parts.join("/");
709        let filename = path_segments[path_segments.len() - 1];
710
711        // Extract version from filename
712        let version = if let Some(zip_pos) = filename.rfind(".zip") {
713            &filename[..zip_pos]
714        } else {
715            return Err(TurboCdnError::config(
716                "Invalid Go proxy filename: must end with .zip".to_string(),
717            ));
718        };
719
720        Ok(ParsedUrl {
721            repository: format!("go/{}", module),
722            version: version.to_string(),
723            filename: filename.to_string(),
724            original_url: original_url.to_string(),
725            source_type: DetectedSourceType::GoProxy,
726        })
727    }
728
729    /// Parse Crates.io URL
730    fn parse_crates_io_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
731        let path = url_obj.path();
732        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
733
734        // Expected format: api/v1/crates/{crate}/{version}/download
735        if path_segments.len() != 6
736            || path_segments[0] != "api"
737            || path_segments[1] != "v1"
738            || path_segments[2] != "crates"
739            || path_segments[5] != "download"
740        {
741            return Err(TurboCdnError::config(
742                "Invalid Crates.io URL format. Expected: https://crates.io/api/v1/crates/{crate}/{version}/download".to_string(),
743            ));
744        }
745
746        let crate_name = path_segments[3];
747        let version = path_segments[4];
748        let filename = format!("{}-{}.crate", crate_name, version);
749
750        Ok(ParsedUrl {
751            repository: format!("crates/{}", crate_name),
752            version: version.to_string(),
753            filename,
754            original_url: original_url.to_string(),
755            source_type: DetectedSourceType::CratesIo,
756        })
757    }
758
759    /// Parse Maven Central URL
760    fn parse_maven_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
761        let path = url_obj.path();
762        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
763
764        // Expected format: maven2/{group_path}/{artifact}/{version}/{artifact}-{version}.jar
765        if path_segments.len() < 5 || path_segments[0] != "maven2" {
766            return Err(TurboCdnError::config(
767                "Invalid Maven URL format. Expected: https://repo1.maven.org/maven2/{group_path}/{artifact}/{version}/{artifact}-{version}.jar".to_string(),
768            ));
769        }
770
771        let artifact = path_segments[path_segments.len() - 3];
772        let version = path_segments[path_segments.len() - 2];
773        let filename = path_segments[path_segments.len() - 1];
774        let group_path = path_segments[1..path_segments.len() - 3].join(".");
775
776        Ok(ParsedUrl {
777            repository: format!("maven/{}.{}", group_path, artifact),
778            version: version.to_string(),
779            filename: filename.to_string(),
780            original_url: original_url.to_string(),
781            source_type: DetectedSourceType::Maven,
782        })
783    }
784
785    /// Parse NuGet URL
786    fn parse_nuget_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
787        let path = url_obj.path();
788        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
789
790        // Expected format: v3-flatcontainer/{package}/{version}/{package}.{version}.nupkg
791        if path_segments.len() != 4 || path_segments[0] != "v3-flatcontainer" {
792            return Err(TurboCdnError::config(
793                "Invalid NuGet URL format. Expected: https://api.nuget.org/v3-flatcontainer/{package}/{version}/{package}.{version}.nupkg".to_string(),
794            ));
795        }
796
797        let package = path_segments[1];
798        let version = path_segments[2];
799        let filename = path_segments[3];
800
801        Ok(ParsedUrl {
802            repository: format!("nuget/{}", package),
803            version: version.to_string(),
804            filename: filename.to_string(),
805            original_url: original_url.to_string(),
806            source_type: DetectedSourceType::NuGet,
807        })
808    }
809
810    /// Parse Docker Hub URL
811    fn parse_docker_hub_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
812        let path = url_obj.path();
813        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
814
815        // Expected format: v2/library/{image}/manifests/{tag}
816        if path_segments.len() != 5
817            || path_segments[0] != "v2"
818            || path_segments[1] != "library"
819            || path_segments[3] != "manifests"
820        {
821            return Err(TurboCdnError::config(
822                "Invalid Docker Hub URL format. Expected: https://registry-1.docker.io/v2/library/{image}/manifests/{tag}".to_string(),
823            ));
824        }
825
826        let image = path_segments[2];
827        let tag = path_segments[4];
828        let filename = format!("{}-{}.tar", image, tag);
829
830        Ok(ParsedUrl {
831            repository: format!("docker/{}", image),
832            version: tag.to_string(),
833            filename,
834            original_url: original_url.to_string(),
835            source_type: DetectedSourceType::DockerHub,
836        })
837    }
838
839    /// Parse SourceForge URL
840    fn parse_sourceforge_url(&self, url_obj: &url::Url, original_url: &str) -> Result<ParsedUrl> {
841        let path = url_obj.path();
842        let path_segments: Vec<&str> = path.trim_start_matches('/').split('/').collect();
843
844        // Expected format: project/{project}/{filename}
845        if path_segments.len() < 3 || path_segments[0] != "project" {
846            return Err(TurboCdnError::config(
847                "Invalid SourceForge URL format. Expected: https://downloads.sourceforge.net/project/{project}/{filename}".to_string(),
848            ));
849        }
850
851        let project = path_segments[1];
852        let filename = path_segments[2..].join("/");
853
854        // Try to extract version from filename
855        let version = self
856            .extract_version_from_filename(&filename)
857            .unwrap_or_else(|| "latest".to_string());
858
859        Ok(ParsedUrl {
860            repository: format!("sourceforge/{}", project),
861            version,
862            filename,
863            original_url: original_url.to_string(),
864            source_type: DetectedSourceType::SourceForge,
865        })
866    }
867
868    /// Get download statistics
869    pub async fn get_stats(&self) -> Result<TurboCdnStats> {
870        self.downloader.get_stats().await
871    }
872
873    /// Perform health check on all sources
874    pub async fn health_check(
875        &self,
876    ) -> Result<std::collections::HashMap<String, sources::HealthStatus>> {
877        self.downloader.health_check().await
878    }
879}
880
881// Note: TurboCdn doesn't implement Clone due to complex internal state
882// Users should create new instances via the builder pattern
883
884impl TurboCdnBuilder {
885    /// Create a new builder with default configuration
886    pub fn new() -> Self {
887        Self {
888            config: TurboCdnConfig::default(),
889            sources: vec![
890                Source::GitHub,
891                Source::JsDelivr,
892                Source::Fastly,
893                Source::Cloudflare,
894            ],
895        }
896    }
897
898    /// Set the configuration
899    pub fn with_config(mut self, config: TurboCdnConfig) -> Self {
900        self.config = config;
901        self
902    }
903
904    /// Set the download sources
905    pub fn with_sources(mut self, sources: &[Source]) -> Self {
906        self.sources = sources.to_vec();
907        self
908    }
909
910    /// Set the region for optimization
911    pub fn with_region(mut self, region: Region) -> Self {
912        self.config.regions.default = region.to_string();
913        self
914    }
915
916    /// Set the download directory
917    pub fn with_download_dir<P: Into<PathBuf>>(self, _dir: P) -> Self {
918        // Note: Download directory is now handled by the cache configuration
919        // This method is kept for API compatibility but doesn't modify anything
920        self
921    }
922
923    /// Enable or disable caching
924    pub fn with_cache(mut self, enabled: bool) -> Self {
925        self.config.performance.cache.enabled = enabled;
926        self
927    }
928
929    /// Set maximum concurrent downloads
930    pub fn with_max_concurrent_downloads(mut self, max: usize) -> Self {
931        self.config.performance.max_concurrent_downloads = max;
932        self
933    }
934
935    /// Build the TurboCdn client
936    pub async fn build(mut self) -> Result<TurboCdn> {
937        // Auto-detect geographic region if enabled
938        if self.config.regions.auto_detect {
939            match self.auto_detect_region().await {
940                Ok(detected_region) => {
941                    info!("Auto-detected region: {:?}", detected_region);
942                    self.config.regions.default = detected_region.to_string();
943                }
944                Err(e) => {
945                    warn!("Failed to auto-detect region: {}, using default", e);
946                }
947            }
948        }
949
950        // Create source manager
951        let mut source_manager = SourceManager::new();
952
953        for source in &self.sources {
954            match source {
955                Source::GitHub => {
956                    if let Some(github_config) = self.config.mirrors.configs.get("github") {
957                        let github_source = GitHubSource::new(github_config.clone())?;
958                        source_manager.add_source(Box::new(github_source));
959                    }
960                }
961                Source::JsDelivr => {
962                    if let Some(jsdelivr_config) = self.config.mirrors.configs.get("jsdelivr") {
963                        let jsdelivr_source = JsDelivrSource::new(jsdelivr_config.clone())?;
964                        source_manager.add_source(Box::new(jsdelivr_source));
965                    }
966                }
967                Source::Fastly => {
968                    if let Some(fastly_config) = self.config.mirrors.configs.get("fastly") {
969                        let fastly_source = FastlySource::new(fastly_config.clone())?;
970                        source_manager.add_source(Box::new(fastly_source));
971                    }
972                }
973                Source::Cloudflare => {
974                    if let Some(cloudflare_config) = self.config.mirrors.configs.get("cloudflare") {
975                        let cloudflare_source = CloudflareSource::new(cloudflare_config.clone())?;
976                        source_manager.add_source(Box::new(cloudflare_source));
977                    }
978                }
979            }
980        }
981
982        // Create components with performance data loading
983        let router = SmartRouter::new_with_data(self.config.clone(), source_manager).await?;
984        let cache_manager = CacheManager::new(self.config.performance.cache.clone()).await?;
985        let compliance_checker = ComplianceChecker::new(self.config.security.clone())?;
986
987        // Create downloader
988        let downloader =
989            Downloader::new(self.config, router, cache_manager, compliance_checker).await?;
990
991        Ok(TurboCdn { downloader })
992    }
993
994    /// Auto-detect user's geographic region
995    async fn auto_detect_region(&self) -> Result<Region> {
996        use crate::geo_detection::GeoDetector;
997
998        let mut detector = GeoDetector::new();
999        detector.detect_region().await
1000    }
1001}
1002
1003impl Default for TurboCdnBuilder {
1004    fn default() -> Self {
1005        Self::new()
1006    }
1007}
1008
1009impl Source {
1010    /// Create a GitHub source
1011    pub fn github() -> Self {
1012        Source::GitHub
1013    }
1014
1015    /// Create a jsDelivr source
1016    pub fn jsdelivr() -> Self {
1017        Source::JsDelivr
1018    }
1019
1020    /// Create a Fastly source
1021    pub fn fastly() -> Self {
1022        Source::Fastly
1023    }
1024
1025    /// Create a Cloudflare source
1026    pub fn cloudflare() -> Self {
1027        Source::Cloudflare
1028    }
1029}
1030
1031/// Statistics for TurboCdn
1032#[derive(Debug, Clone, Default)]
1033pub struct TurboCdnStats {
1034    /// Total downloads
1035    pub total_downloads: u64,
1036
1037    /// Successful downloads
1038    pub successful_downloads: u64,
1039
1040    /// Failed downloads
1041    pub failed_downloads: u64,
1042
1043    /// Total bytes downloaded
1044    pub total_bytes: u64,
1045
1046    /// Cache hit rate
1047    pub cache_hit_rate: f64,
1048
1049    /// Average download speed in bytes per second
1050    pub average_speed: f64,
1051}
1052
1053/// Async API module for external integrations (like vx)
1054pub mod async_api {
1055    use super::*;
1056    use std::sync::Arc;
1057    use tokio::sync::Mutex;
1058
1059    /// Async wrapper for TurboCdn that provides thread-safe access
1060    #[derive(Debug, Clone)]
1061    pub struct AsyncTurboCdn {
1062        inner: Arc<Mutex<TurboCdn>>,
1063    }
1064
1065    impl AsyncTurboCdn {
1066        /// Create a new AsyncTurboCdn instance
1067        pub async fn new() -> Result<Self> {
1068            let turbo_cdn = TurboCdn::new().await?;
1069            Ok(Self {
1070                inner: Arc::new(Mutex::new(turbo_cdn)),
1071            })
1072        }
1073
1074        /// Create a new AsyncTurboCdn instance with custom configuration
1075        pub async fn with_config(config: TurboCdnConfig) -> Result<Self> {
1076            let turbo_cdn = TurboCdn::builder().with_config(config).build().await?;
1077            Ok(Self {
1078                inner: Arc::new(Mutex::new(turbo_cdn)),
1079            })
1080        }
1081
1082        /// Create a new AsyncTurboCdn instance with builder
1083        pub async fn with_builder(builder: TurboCdnBuilder) -> Result<Self> {
1084            let turbo_cdn = builder.build().await?;
1085            Ok(Self {
1086                inner: Arc::new(Mutex::new(turbo_cdn)),
1087            })
1088        }
1089
1090        /// Download from any supported URL with automatic optimization (async version)
1091        pub async fn download_from_url_async(
1092            &self,
1093            url: &str,
1094            options: Option<DownloadOptions>,
1095        ) -> Result<DownloadResult> {
1096            let mut client = self.inner.lock().await;
1097            client.download_from_url(url, options).await
1098        }
1099
1100        /// Get optimal CDN URL without downloading (async version)
1101        pub async fn get_optimal_url_async(&self, url: &str) -> Result<String> {
1102            let client = self.inner.lock().await;
1103            client.get_optimal_url(url).await
1104        }
1105
1106        /// Parse URL into components (async version)
1107        pub async fn parse_url_async(&self, url: &str) -> Result<ParsedUrl> {
1108            let client = self.inner.lock().await;
1109            client.parse_url(url)
1110        }
1111
1112        /// Download a file by repository, version, and filename (async version)
1113        pub async fn download_async(
1114            &self,
1115            repository: &str,
1116            version: &str,
1117            file_name: &str,
1118            options: Option<DownloadOptions>,
1119        ) -> Result<DownloadResult> {
1120            let mut client = self.inner.lock().await;
1121            client
1122                .download(repository, version, file_name, options.unwrap_or_default())
1123                .await
1124        }
1125
1126        /// Get repository metadata (async version)
1127        pub async fn get_repository_metadata_async(
1128            &self,
1129            repository: &str,
1130        ) -> Result<sources::RepositoryMetadata> {
1131            let client = self.inner.lock().await;
1132            client.get_repository_metadata(repository).await
1133        }
1134
1135        /// Extract version from filename (async version)
1136        pub async fn extract_version_from_filename_async(&self, filename: &str) -> Option<String> {
1137            let client = self.inner.lock().await;
1138            client.extract_version_from_filename(filename)
1139        }
1140
1141        /// Get download statistics (async version)
1142        pub async fn get_stats_async(&self) -> Result<TurboCdnStats> {
1143            let client = self.inner.lock().await;
1144            client.get_stats().await
1145        }
1146
1147        /// Perform health check on all sources (async version)
1148        pub async fn health_check_async(
1149            &self,
1150        ) -> Result<std::collections::HashMap<String, sources::HealthStatus>> {
1151            let client = self.inner.lock().await;
1152            client.health_check().await
1153        }
1154    }
1155
1156    /// Async builder for AsyncTurboCdn
1157    #[derive(Debug)]
1158    pub struct AsyncTurboCdnBuilder {
1159        builder: TurboCdnBuilder,
1160    }
1161
1162    impl AsyncTurboCdnBuilder {
1163        /// Create a new async builder
1164        pub fn new() -> Self {
1165            Self {
1166                builder: TurboCdnBuilder::new(),
1167            }
1168        }
1169
1170        /// Set the configuration
1171        pub fn with_config(mut self, config: TurboCdnConfig) -> Self {
1172            self.builder = self.builder.with_config(config);
1173            self
1174        }
1175
1176        /// Set the download sources
1177        pub fn with_sources(mut self, sources: &[Source]) -> Self {
1178            self.builder = self.builder.with_sources(sources);
1179            self
1180        }
1181
1182        /// Set the region for optimization
1183        pub fn with_region(mut self, region: Region) -> Self {
1184            self.builder = self.builder.with_region(region);
1185            self
1186        }
1187
1188        /// Set the download directory
1189        pub fn with_download_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
1190            self.builder = self.builder.with_download_dir(dir);
1191            self
1192        }
1193
1194        /// Enable or disable caching
1195        pub fn with_cache(mut self, enabled: bool) -> Self {
1196            self.builder = self.builder.with_cache(enabled);
1197            self
1198        }
1199
1200        /// Set maximum concurrent downloads
1201        pub fn with_max_concurrent_downloads(mut self, max: usize) -> Self {
1202            self.builder = self.builder.with_max_concurrent_downloads(max);
1203            self
1204        }
1205
1206        /// Build the AsyncTurboCdn client
1207        pub async fn build(self) -> Result<AsyncTurboCdn> {
1208            AsyncTurboCdn::with_builder(self.builder).await
1209        }
1210    }
1211
1212    impl Default for AsyncTurboCdnBuilder {
1213        fn default() -> Self {
1214            Self::new()
1215        }
1216    }
1217
1218    /// Convenience functions for quick async operations
1219    pub mod quick {
1220        use super::*;
1221
1222        /// Quick download from URL with default settings
1223        pub async fn download_url(url: &str) -> Result<DownloadResult> {
1224            let client = AsyncTurboCdn::new().await?;
1225            client.download_from_url_async(url, None).await
1226        }
1227
1228        /// Quick URL optimization
1229        pub async fn optimize_url(url: &str) -> Result<String> {
1230            let client = AsyncTurboCdn::new().await?;
1231            client.get_optimal_url_async(url).await
1232        }
1233
1234        /// Quick URL parsing
1235        pub async fn parse_url(url: &str) -> Result<ParsedUrl> {
1236            let client = AsyncTurboCdn::new().await?;
1237            client.parse_url_async(url).await
1238        }
1239
1240        /// Quick repository download
1241        pub async fn download_repository(
1242            repository: &str,
1243            version: &str,
1244            file_name: &str,
1245        ) -> Result<DownloadResult> {
1246            let client = AsyncTurboCdn::new().await?;
1247            client
1248                .download_async(repository, version, file_name, None)
1249                .await
1250        }
1251    }
1252}
1253
1254/// Initialize tracing for the library
1255pub fn init_tracing() {
1256    use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
1257
1258    tracing_subscriber::registry()
1259        .with(
1260            tracing_subscriber::EnvFilter::try_from_default_env()
1261                .unwrap_or_else(|_| "turbo_cdn=info".into()),
1262        )
1263        .with(tracing_subscriber::fmt::layer())
1264        .init();
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269    use super::*;
1270
1271    #[tokio::test]
1272    async fn test_builder() {
1273        let _builder = TurboCdn::builder()
1274            .with_region(Region::Global)
1275            .with_cache(true);
1276
1277        // Just test that the builder can be created without panicking
1278        // The fact that we reach this point means the builder works correctly
1279    }
1280
1281    #[test]
1282    fn test_source_creation() {
1283        let _github = Source::github();
1284        let _jsdelivr = Source::jsdelivr();
1285        let _fastly = Source::fastly();
1286        let _cloudflare = Source::cloudflare();
1287    }
1288
1289    #[tokio::test]
1290    async fn test_turbo_cdn_new() {
1291        let result = TurboCdn::new().await;
1292        assert!(result.is_ok());
1293    }
1294
1295    #[test]
1296    fn test_turbo_cdn_stats_creation() {
1297        let stats = TurboCdnStats {
1298            total_downloads: 100,
1299            successful_downloads: 95,
1300            failed_downloads: 5,
1301            total_bytes: 1024 * 1024,
1302            cache_hit_rate: 0.8,
1303            average_speed: 1000.0,
1304        };
1305
1306        assert_eq!(stats.total_downloads, 100);
1307        assert_eq!(stats.successful_downloads, 95);
1308        assert_eq!(stats.failed_downloads, 5);
1309        assert_eq!(stats.total_bytes, 1024 * 1024);
1310        assert_eq!(stats.cache_hit_rate, 0.8);
1311        assert_eq!(stats.average_speed, 1000.0);
1312    }
1313
1314    #[test]
1315    fn test_parsed_url_creation() {
1316        let parsed = ParsedUrl {
1317            repository: "owner/repo".to_string(),
1318            version: "v1.0.0".to_string(),
1319            filename: "file.zip".to_string(),
1320            original_url: "https://github.com/owner/repo/releases/download/v1.0.0/file.zip"
1321                .to_string(),
1322            source_type: DetectedSourceType::GitHub,
1323        };
1324
1325        assert_eq!(parsed.repository, "owner/repo");
1326        assert_eq!(parsed.version, "v1.0.0");
1327        assert_eq!(parsed.filename, "file.zip");
1328        assert_eq!(parsed.source_type, DetectedSourceType::GitHub);
1329    }
1330
1331    #[test]
1332    fn test_download_result_creation() {
1333        use std::path::PathBuf;
1334        use std::time::Duration;
1335
1336        let result = DownloadResult {
1337            path: PathBuf::from("/tmp/file.zip"),
1338            size: 1024,
1339            duration: Duration::from_secs(1),
1340            speed: 1024.0,
1341            source: "github".to_string(),
1342            url: "https://github.com/owner/repo/releases/download/v1.0.0/file.zip".to_string(),
1343            from_cache: false,
1344            checksum: None,
1345        };
1346
1347        assert_eq!(result.path, PathBuf::from("/tmp/file.zip"));
1348        assert_eq!(result.size, 1024);
1349        assert_eq!(result.duration, Duration::from_secs(1));
1350        assert_eq!(result.speed, 1024.0);
1351        assert_eq!(result.source, "github");
1352        assert!(!result.from_cache);
1353        assert!(result.checksum.is_none());
1354    }
1355
1356    #[test]
1357    fn test_builder_configuration() {
1358        let builder = TurboCdn::builder()
1359            .with_cache(false)
1360            .with_max_concurrent_downloads(10);
1361
1362        assert!(!builder.config.performance.cache.enabled);
1363        assert_eq!(builder.config.performance.max_concurrent_downloads, 10);
1364        assert_eq!(builder.sources.len(), 4); // Default sources
1365    }
1366}