1pub 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
64pub 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#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct ParsedUrl {
81 pub repository: String,
83 pub version: String,
85 pub filename: String,
87 pub original_url: String,
89 pub source_type: DetectedSourceType,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum DetectedSourceType {
96 GitHub,
98 JsDelivr,
100 Fastly,
102 Cloudflare,
104 Npm,
106 PyPI,
108 GoProxy,
110 CratesIo,
112 Maven,
114 NuGet,
116 DockerHub,
118 GitLab,
120 Bitbucket,
122 SourceForge,
124 Other(String),
126}
127
128#[derive(Debug)]
130pub struct TurboCdn {
131 downloader: Downloader,
132}
133
134#[derive(Debug)]
136pub struct TurboCdnBuilder {
137 config: TurboCdnConfig,
138 sources: Vec<Source>,
139}
140
141#[derive(Debug, Clone)]
143pub enum Source {
144 GitHub,
145 JsDelivr,
146 Fastly,
147 Cloudflare,
148}
149
150impl TurboCdn {
151 pub fn builder() -> TurboCdnBuilder {
153 TurboCdnBuilder::new()
154 }
155
156 pub async fn new() -> Result<Self> {
158 Self::builder().build().await
159 }
160
161 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 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 pub async fn get_optimal_url(&self, url: &str) -> Result<String> {
276 let parsed_url = self.parse_url(url)?;
277
278 self.downloader
280 .get_optimal_url(
281 &parsed_url.repository,
282 &parsed_url.version,
283 &parsed_url.filename,
284 )
285 .await
286 }
287
288 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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), version: version.to_string(),
562 filename,
563 original_url: original_url.to_string(),
564 source_type: DetectedSourceType::Cloudflare,
565 })
566 }
567
568 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 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 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), 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 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 pub fn extract_version_from_filename(&self, filename: &str) -> Option<String> {
633 let patterns = [
635 r"v?(\d+\.\d+\.\d+)", r"v?(\d+\.\d+)", r"(\d{4}-\d{2}-\d{2})", r"(\d{8})", ];
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn get_stats(&self) -> Result<TurboCdnStats> {
870 self.downloader.get_stats().await
871 }
872
873 pub async fn health_check(
875 &self,
876 ) -> Result<std::collections::HashMap<String, sources::HealthStatus>> {
877 self.downloader.health_check().await
878 }
879}
880
881impl TurboCdnBuilder {
885 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 pub fn with_config(mut self, config: TurboCdnConfig) -> Self {
900 self.config = config;
901 self
902 }
903
904 pub fn with_sources(mut self, sources: &[Source]) -> Self {
906 self.sources = sources.to_vec();
907 self
908 }
909
910 pub fn with_region(mut self, region: Region) -> Self {
912 self.config.regions.default = region.to_string();
913 self
914 }
915
916 pub fn with_download_dir<P: Into<PathBuf>>(self, _dir: P) -> Self {
918 self
921 }
922
923 pub fn with_cache(mut self, enabled: bool) -> Self {
925 self.config.performance.cache.enabled = enabled;
926 self
927 }
928
929 pub fn with_max_concurrent_downloads(mut self, max: usize) -> Self {
931 self.config.performance.max_concurrent_downloads = max;
932 self
933 }
934
935 pub async fn build(mut self) -> Result<TurboCdn> {
937 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 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 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 let downloader =
989 Downloader::new(self.config, router, cache_manager, compliance_checker).await?;
990
991 Ok(TurboCdn { downloader })
992 }
993
994 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 pub fn github() -> Self {
1012 Source::GitHub
1013 }
1014
1015 pub fn jsdelivr() -> Self {
1017 Source::JsDelivr
1018 }
1019
1020 pub fn fastly() -> Self {
1022 Source::Fastly
1023 }
1024
1025 pub fn cloudflare() -> Self {
1027 Source::Cloudflare
1028 }
1029}
1030
1031#[derive(Debug, Clone, Default)]
1033pub struct TurboCdnStats {
1034 pub total_downloads: u64,
1036
1037 pub successful_downloads: u64,
1039
1040 pub failed_downloads: u64,
1042
1043 pub total_bytes: u64,
1045
1046 pub cache_hit_rate: f64,
1048
1049 pub average_speed: f64,
1051}
1052
1053pub mod async_api {
1055 use super::*;
1056 use std::sync::Arc;
1057 use tokio::sync::Mutex;
1058
1059 #[derive(Debug, Clone)]
1061 pub struct AsyncTurboCdn {
1062 inner: Arc<Mutex<TurboCdn>>,
1063 }
1064
1065 impl AsyncTurboCdn {
1066 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 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 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 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 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 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 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 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 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 pub async fn get_stats_async(&self) -> Result<TurboCdnStats> {
1143 let client = self.inner.lock().await;
1144 client.get_stats().await
1145 }
1146
1147 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 #[derive(Debug)]
1158 pub struct AsyncTurboCdnBuilder {
1159 builder: TurboCdnBuilder,
1160 }
1161
1162 impl AsyncTurboCdnBuilder {
1163 pub fn new() -> Self {
1165 Self {
1166 builder: TurboCdnBuilder::new(),
1167 }
1168 }
1169
1170 pub fn with_config(mut self, config: TurboCdnConfig) -> Self {
1172 self.builder = self.builder.with_config(config);
1173 self
1174 }
1175
1176 pub fn with_sources(mut self, sources: &[Source]) -> Self {
1178 self.builder = self.builder.with_sources(sources);
1179 self
1180 }
1181
1182 pub fn with_region(mut self, region: Region) -> Self {
1184 self.builder = self.builder.with_region(region);
1185 self
1186 }
1187
1188 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 pub fn with_cache(mut self, enabled: bool) -> Self {
1196 self.builder = self.builder.with_cache(enabled);
1197 self
1198 }
1199
1200 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 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 pub mod quick {
1220 use super::*;
1221
1222 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 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 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 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
1254pub 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 }
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); }
1366}