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