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;
59
60use std::path::PathBuf;
61use tracing::{info, warn};
62
63pub 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#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct ParsedUrl {
79 pub repository: String,
81 pub version: String,
83 pub filename: String,
85 pub original_url: String,
87 pub source_type: DetectedSourceType,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum DetectedSourceType {
94 GitHub,
96 JsDelivr,
98 Fastly,
100 Cloudflare,
102 Npm,
104 PyPI,
106 GoProxy,
108 CratesIo,
110 Maven,
112 NuGet,
114 DockerHub,
116 GitLab,
118 Bitbucket,
120 SourceForge,
122 Other(String),
124}
125
126#[derive(Debug)]
128pub struct TurboCdn {
129 downloader: Downloader,
130}
131
132#[derive(Debug)]
134pub struct TurboCdnBuilder {
135 config: TurboCdnConfig,
136 sources: Vec<Source>,
137}
138
139#[derive(Debug, Clone)]
141pub enum Source {
142 GitHub,
143 JsDelivr,
144 Fastly,
145 Cloudflare,
146}
147
148impl TurboCdn {
149 pub fn builder() -> TurboCdnBuilder {
151 TurboCdnBuilder::new()
152 }
153
154 pub async fn new() -> Result<Self> {
156 Self::builder().build().await
157 }
158
159 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 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 pub async fn get_optimal_url(&self, url: &str) -> Result<String> {
274 let parsed_url = self.parse_url(url)?;
275
276 self.downloader
278 .get_optimal_url(
279 &parsed_url.repository,
280 &parsed_url.version,
281 &parsed_url.filename,
282 )
283 .await
284 }
285
286 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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), version: version.to_string(),
560 filename,
561 original_url: original_url.to_string(),
562 source_type: DetectedSourceType::Cloudflare,
563 })
564 }
565
566 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 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 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), 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 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 pub fn extract_version_from_filename(&self, filename: &str) -> Option<String> {
631 let patterns = [
633 r"v?(\d+\.\d+\.\d+)", r"v?(\d+\.\d+)", r"(\d{4}-\d{2}-\d{2})", r"(\d{8})", ];
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn get_stats(&self) -> Result<TurboCdnStats> {
868 self.downloader.get_stats().await
869 }
870
871 pub async fn health_check(
873 &self,
874 ) -> Result<std::collections::HashMap<String, sources::HealthStatus>> {
875 self.downloader.health_check().await
876 }
877}
878
879impl TurboCdnBuilder {
883 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 pub fn with_config(mut self, config: TurboCdnConfig) -> Self {
898 self.config = config;
899 self
900 }
901
902 pub fn with_sources(mut self, sources: &[Source]) -> Self {
904 self.sources = sources.to_vec();
905 self
906 }
907
908 pub fn with_region(mut self, region: Region) -> Self {
910 self.config.regions.default = region.to_string();
911 self
912 }
913
914 pub fn with_download_dir<P: Into<PathBuf>>(self, _dir: P) -> Self {
916 self
919 }
920
921 pub fn with_cache(mut self, enabled: bool) -> Self {
923 self.config.performance.cache.enabled = enabled;
924 self
925 }
926
927 pub fn with_max_concurrent_downloads(mut self, max: usize) -> Self {
929 self.config.performance.max_concurrent_downloads = max;
930 self
931 }
932
933 pub async fn build(mut self) -> Result<TurboCdn> {
935 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 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 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 let downloader =
987 Downloader::new(self.config, router, cache_manager, compliance_checker).await?;
988
989 Ok(TurboCdn { downloader })
990 }
991
992 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 pub fn github() -> Self {
1010 Source::GitHub
1011 }
1012
1013 pub fn jsdelivr() -> Self {
1015 Source::JsDelivr
1016 }
1017
1018 pub fn fastly() -> Self {
1020 Source::Fastly
1021 }
1022
1023 pub fn cloudflare() -> Self {
1025 Source::Cloudflare
1026 }
1027}
1028
1029#[derive(Debug, Clone, Default)]
1031pub struct TurboCdnStats {
1032 pub total_downloads: u64,
1034
1035 pub successful_downloads: u64,
1037
1038 pub failed_downloads: u64,
1040
1041 pub total_bytes: u64,
1043
1044 pub cache_hit_rate: f64,
1046
1047 pub average_speed: f64,
1049}
1050
1051pub mod async_api {
1053 use super::*;
1054 use std::sync::Arc;
1055 use tokio::sync::Mutex;
1056
1057 #[derive(Debug, Clone)]
1059 pub struct AsyncTurboCdn {
1060 inner: Arc<Mutex<TurboCdn>>,
1061 }
1062
1063 impl AsyncTurboCdn {
1064 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 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 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 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 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 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 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 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 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 pub async fn get_stats_async(&self) -> Result<TurboCdnStats> {
1141 let client = self.inner.lock().await;
1142 client.get_stats().await
1143 }
1144
1145 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 #[derive(Debug)]
1156 pub struct AsyncTurboCdnBuilder {
1157 builder: TurboCdnBuilder,
1158 }
1159
1160 impl AsyncTurboCdnBuilder {
1161 pub fn new() -> Self {
1163 Self {
1164 builder: TurboCdnBuilder::new(),
1165 }
1166 }
1167
1168 pub fn with_config(mut self, config: TurboCdnConfig) -> Self {
1170 self.builder = self.builder.with_config(config);
1171 self
1172 }
1173
1174 pub fn with_sources(mut self, sources: &[Source]) -> Self {
1176 self.builder = self.builder.with_sources(sources);
1177 self
1178 }
1179
1180 pub fn with_region(mut self, region: Region) -> Self {
1182 self.builder = self.builder.with_region(region);
1183 self
1184 }
1185
1186 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 pub fn with_cache(mut self, enabled: bool) -> Self {
1194 self.builder = self.builder.with_cache(enabled);
1195 self
1196 }
1197
1198 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 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 pub mod quick {
1218 use super::*;
1219
1220 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 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 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 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
1252pub 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 }
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); }
1364}