Skip to main content

torsh_package/
cdn.rs

1//! Content Delivery Network (CDN) support for fast package distribution
2//!
3//! This module provides CDN integration for efficient package distribution
4//! with geographic load balancing, caching, and edge node management.
5
6use std::collections::HashMap;
7use std::time::{Duration, SystemTime};
8
9use serde::{Deserialize, Serialize};
10use torsh_core::error::{Result, TorshError};
11
12/// CDN provider type
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub enum CdnProvider {
15    /// Cloudflare CDN
16    Cloudflare,
17    /// AWS CloudFront
18    CloudFront,
19    /// Google Cloud CDN
20    GoogleCdn,
21    /// Azure CDN
22    AzureCdn,
23    /// Fastly CDN
24    Fastly,
25    /// Custom CDN provider
26    Custom(String),
27}
28
29/// CDN configuration
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CdnConfig {
32    /// CDN provider
33    pub provider: CdnProvider,
34    /// CDN endpoint URL
35    pub endpoint: String,
36    /// API key for CDN management
37    pub api_key: Option<String>,
38    /// Cache TTL in seconds
39    pub cache_ttl: u64,
40    /// Enable compression at edge
41    pub edge_compression: bool,
42    /// Geographic regions to use
43    pub regions: Vec<CdnRegion>,
44    /// Custom headers to add
45    pub custom_headers: HashMap<String, String>,
46}
47
48/// CDN geographic region
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub enum CdnRegion {
51    /// North America
52    NorthAmerica,
53    /// Europe
54    Europe,
55    /// Asia Pacific
56    AsiaPacific,
57    /// South America
58    SouthAmerica,
59    /// Africa
60    Africa,
61    /// Middle East
62    MiddleEast,
63    /// Oceania
64    Oceania,
65}
66
67/// CDN cache control settings
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CacheControl {
70    /// Maximum age in seconds
71    pub max_age: u64,
72    /// Enable public caching
73    pub public: bool,
74    /// Enable private caching
75    pub private: bool,
76    /// No cache directive
77    pub no_cache: bool,
78    /// No store directive
79    pub no_store: bool,
80    /// Must revalidate directive
81    pub must_revalidate: bool,
82}
83
84/// CDN edge node information
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct EdgeNode {
87    /// Node ID
88    pub id: String,
89    /// Node location
90    pub location: String,
91    /// Region
92    pub region: CdnRegion,
93    /// Node status
94    pub status: EdgeNodeStatus,
95    /// Current load percentage (0-100)
96    pub load: u8,
97    /// Latency in milliseconds
98    pub latency_ms: u64,
99    /// Bandwidth capacity in Mbps
100    pub bandwidth_mbps: u64,
101}
102
103/// Edge node status
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub enum EdgeNodeStatus {
106    /// Node is active and healthy
107    Active,
108    /// Node is degraded (partial functionality)
109    Degraded,
110    /// Node is offline
111    Offline,
112    /// Node is under maintenance
113    Maintenance,
114}
115
116/// CDN statistics
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct CdnStatistics {
119    /// Total requests served
120    pub total_requests: u64,
121    /// Cache hit rate (0.0-1.0)
122    pub cache_hit_rate: f64,
123    /// Total bytes transferred
124    pub bytes_transferred: u64,
125    /// Average response time in milliseconds
126    pub avg_response_ms: f64,
127    /// Requests by region
128    pub requests_by_region: HashMap<String, u64>,
129    /// Error rate (0.0-1.0)
130    pub error_rate: f64,
131}
132
133/// CDN manager for package distribution
134pub struct CdnManager {
135    /// CDN configuration
136    config: CdnConfig,
137    /// Available edge nodes
138    edge_nodes: Vec<EdgeNode>,
139    /// CDN statistics
140    statistics: CdnStatistics,
141    /// Cache entries
142    cache: HashMap<String, CachedItem>,
143}
144
145/// Cached item metadata
146#[derive(Debug, Clone)]
147struct CachedItem {
148    /// Cache key
149    _key: String,
150    /// URL to cached content
151    url: String,
152    /// Cache expiration time
153    expires_at: SystemTime,
154    /// Content size in bytes
155    _size: u64,
156    /// Number of hits
157    hits: u64,
158}
159
160impl Default for CdnConfig {
161    fn default() -> Self {
162        Self {
163            provider: CdnProvider::Cloudflare,
164            endpoint: "https://cdn.torsh.rs".to_string(),
165            api_key: None,
166            cache_ttl: 86400, // 24 hours
167            edge_compression: true,
168            regions: vec![
169                CdnRegion::NorthAmerica,
170                CdnRegion::Europe,
171                CdnRegion::AsiaPacific,
172            ],
173            custom_headers: HashMap::new(),
174        }
175    }
176}
177
178impl CdnConfig {
179    /// Create a new CDN configuration
180    pub fn new(provider: CdnProvider, endpoint: String) -> Self {
181        Self {
182            provider,
183            endpoint,
184            ..Default::default()
185        }
186    }
187
188    /// Set API key
189    pub fn with_api_key(mut self, api_key: String) -> Self {
190        self.api_key = Some(api_key);
191        self
192    }
193
194    /// Set cache TTL
195    pub fn with_cache_ttl(mut self, ttl: u64) -> Self {
196        self.cache_ttl = ttl;
197        self
198    }
199
200    /// Enable edge compression
201    pub fn with_edge_compression(mut self, enabled: bool) -> Self {
202        self.edge_compression = enabled;
203        self
204    }
205
206    /// Add region
207    pub fn add_region(mut self, region: CdnRegion) -> Self {
208        if !self.regions.contains(&region) {
209            self.regions.push(region);
210        }
211        self
212    }
213
214    /// Add custom header
215    pub fn add_header(mut self, key: String, value: String) -> Self {
216        self.custom_headers.insert(key, value);
217        self
218    }
219
220    /// Validate configuration
221    pub fn validate(&self) -> Result<()> {
222        if self.endpoint.is_empty() {
223            return Err(TorshError::InvalidArgument(
224                "CDN endpoint cannot be empty".to_string(),
225            ));
226        }
227
228        if self.regions.is_empty() {
229            return Err(TorshError::InvalidArgument(
230                "At least one region must be configured".to_string(),
231            ));
232        }
233
234        if self.cache_ttl == 0 {
235            return Err(TorshError::InvalidArgument(
236                "Cache TTL must be greater than zero".to_string(),
237            ));
238        }
239
240        Ok(())
241    }
242}
243
244impl Default for CacheControl {
245    fn default() -> Self {
246        Self {
247            max_age: 86400, // 24 hours
248            public: true,
249            private: false,
250            no_cache: false,
251            no_store: false,
252            must_revalidate: false,
253        }
254    }
255}
256
257impl CacheControl {
258    /// Create cache control for immutable content
259    pub fn immutable() -> Self {
260        Self {
261            max_age: 31536000, // 1 year
262            public: true,
263            private: false,
264            no_cache: false,
265            no_store: false,
266            must_revalidate: false,
267        }
268    }
269
270    /// Create cache control for no caching
271    pub fn no_cache() -> Self {
272        Self {
273            max_age: 0,
274            public: false,
275            private: false,
276            no_cache: true,
277            no_store: true,
278            must_revalidate: true,
279        }
280    }
281
282    /// Generate Cache-Control header value
283    pub fn to_header(&self) -> String {
284        let mut parts = Vec::new();
285
286        if self.public {
287            parts.push("public".to_string());
288        }
289        if self.private {
290            parts.push("private".to_string());
291        }
292        if self.no_cache {
293            parts.push("no-cache".to_string());
294        }
295        if self.no_store {
296            parts.push("no-store".to_string());
297        }
298        if self.must_revalidate {
299            parts.push("must-revalidate".to_string());
300        }
301        if self.max_age > 0 {
302            parts.push(format!("max-age={}", self.max_age));
303        }
304
305        parts.join(", ")
306    }
307}
308
309impl EdgeNode {
310    /// Create a new edge node
311    pub fn new(id: String, location: String, region: CdnRegion) -> Self {
312        Self {
313            id,
314            location,
315            region,
316            status: EdgeNodeStatus::Active,
317            load: 0,
318            latency_ms: 0,
319            bandwidth_mbps: 1000, // Default 1 Gbps
320        }
321    }
322
323    /// Check if node is healthy
324    pub fn is_healthy(&self) -> bool {
325        matches!(self.status, EdgeNodeStatus::Active) && self.load < 90
326    }
327
328    /// Check if node is available
329    pub fn is_available(&self) -> bool {
330        matches!(
331            self.status,
332            EdgeNodeStatus::Active | EdgeNodeStatus::Degraded
333        )
334    }
335
336    /// Calculate node score for selection
337    pub fn calculate_score(&self) -> f64 {
338        if !self.is_available() {
339            return 0.0;
340        }
341
342        // Lower is better for latency and load
343        let latency_score = 1.0 / (1.0 + self.latency_ms as f64 / 100.0);
344        let load_score = 1.0 - (self.load as f64 / 100.0);
345        let bandwidth_score = (self.bandwidth_mbps as f64).min(10000.0) / 10000.0;
346
347        // Weighted average: latency 40%, load 40%, bandwidth 20%
348        (latency_score * 0.4) + (load_score * 0.4) + (bandwidth_score * 0.2)
349    }
350}
351
352impl Default for CdnStatistics {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358impl CdnStatistics {
359    /// Create new CDN statistics
360    pub fn new() -> Self {
361        Self {
362            total_requests: 0,
363            cache_hit_rate: 0.0,
364            bytes_transferred: 0,
365            avg_response_ms: 0.0,
366            requests_by_region: HashMap::new(),
367            error_rate: 0.0,
368        }
369    }
370
371    /// Record a request
372    pub fn record_request(&mut self, region: &str, bytes: u64, response_ms: u64, cache_hit: bool) {
373        self.total_requests += 1;
374        self.bytes_transferred += bytes;
375
376        // Update cache hit rate (moving average)
377        let hit_value = if cache_hit { 1.0 } else { 0.0 };
378        self.cache_hit_rate = (self.cache_hit_rate * (self.total_requests - 1) as f64 + hit_value)
379            / self.total_requests as f64;
380
381        // Update average response time
382        self.avg_response_ms = (self.avg_response_ms * (self.total_requests - 1) as f64
383            + response_ms as f64)
384            / self.total_requests as f64;
385
386        // Update region statistics
387        *self
388            .requests_by_region
389            .entry(region.to_string())
390            .or_insert(0) += 1;
391    }
392
393    /// Record an error
394    pub fn record_error(&mut self) {
395        self.total_requests += 1;
396        self.error_rate =
397            (self.error_rate * (self.total_requests - 1) as f64 + 1.0) / self.total_requests as f64;
398    }
399}
400
401impl Default for CdnManager {
402    fn default() -> Self {
403        Self::new(CdnConfig::default())
404    }
405}
406
407impl CdnManager {
408    /// Create a new CDN manager
409    pub fn new(config: CdnConfig) -> Self {
410        Self {
411            config,
412            edge_nodes: Vec::new(),
413            statistics: CdnStatistics::new(),
414            cache: HashMap::new(),
415        }
416    }
417
418    /// Add an edge node
419    pub fn add_edge_node(&mut self, node: EdgeNode) {
420        self.edge_nodes.push(node);
421    }
422
423    /// Get best edge node for a region
424    pub fn get_best_node(&self, region: &CdnRegion) -> Option<&EdgeNode> {
425        let mut candidates: Vec<_> = self
426            .edge_nodes
427            .iter()
428            .filter(|n| n.is_available() && &n.region == region)
429            .collect();
430
431        if candidates.is_empty() {
432            // Fall back to any available node
433            candidates = self
434                .edge_nodes
435                .iter()
436                .filter(|n| n.is_available())
437                .collect();
438        }
439
440        candidates
441            .iter()
442            .max_by(|a, b| {
443                a.calculate_score()
444                    .partial_cmp(&b.calculate_score())
445                    .unwrap_or(std::cmp::Ordering::Equal)
446            })
447            .copied()
448    }
449
450    /// Upload package to CDN
451    pub fn upload_package(
452        &mut self,
453        package_name: &str,
454        version: &str,
455        _data: &[u8],
456    ) -> Result<String> {
457        let cache_key = format!("{}/{}", package_name, version);
458
459        // Generate CDN URL
460        let url = format!(
461            "{}/packages/{}/{}",
462            self.config.endpoint, package_name, version
463        );
464
465        // In production, would upload to CDN
466        // For now, create cache entry
467        let cache_item = CachedItem {
468            _key: cache_key.clone(),
469            url: url.clone(),
470            expires_at: SystemTime::now() + Duration::from_secs(self.config.cache_ttl),
471            _size: _data.len() as u64,
472            hits: 0,
473        };
474
475        self.cache.insert(cache_key, cache_item);
476
477        Ok(url)
478    }
479
480    /// Get package URL from CDN
481    pub fn get_package_url(&mut self, package_name: &str, version: &str) -> Option<String> {
482        let cache_key = format!("{}/{}", package_name, version);
483
484        if let Some(item) = self.cache.get_mut(&cache_key) {
485            // Check if cache entry is still valid
486            if SystemTime::now() < item.expires_at {
487                item.hits += 1;
488                return Some(item.url.clone());
489            } else {
490                // Cache expired
491                self.cache.remove(&cache_key);
492            }
493        }
494
495        None
496    }
497
498    /// Purge cache for a package
499    pub fn purge_cache(&mut self, package_name: &str, version: Option<&str>) -> Result<()> {
500        if let Some(ver) = version {
501            // Purge specific version
502            let cache_key = format!("{}/{}", package_name, ver);
503            self.cache.remove(&cache_key);
504        } else {
505            // Purge all versions
506            let prefix = format!("{}/", package_name);
507            self.cache.retain(|k, _| !k.starts_with(&prefix));
508        }
509
510        Ok(())
511    }
512
513    /// Get CDN statistics
514    pub fn get_statistics(&self) -> &CdnStatistics {
515        &self.statistics
516    }
517
518    /// Get cache hit rate
519    pub fn get_cache_hit_rate(&self) -> f64 {
520        self.statistics.cache_hit_rate
521    }
522
523    /// Get healthy edge nodes
524    pub fn get_healthy_nodes(&self) -> Vec<&EdgeNode> {
525        self.edge_nodes.iter().filter(|n| n.is_healthy()).collect()
526    }
527
528    /// Get edge nodes by region
529    pub fn get_nodes_by_region(&self, region: &CdnRegion) -> Vec<&EdgeNode> {
530        self.edge_nodes
531            .iter()
532            .filter(|n| &n.region == region)
533            .collect()
534    }
535
536    /// Generate cache control header
537    pub fn generate_cache_control(&self, package_version: &str) -> String {
538        // Use immutable cache for versioned packages
539        if !package_version.is_empty() {
540            CacheControl::immutable().to_header()
541        } else {
542            CacheControl::default().to_header()
543        }
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    #[test]
552    fn test_cdn_config() {
553        let config = CdnConfig::new(
554            CdnProvider::Cloudflare,
555            "https://cdn.example.com".to_string(),
556        )
557        .with_cache_ttl(3600)
558        .with_edge_compression(true)
559        .add_region(CdnRegion::NorthAmerica);
560
561        assert_eq!(config.provider, CdnProvider::Cloudflare);
562        assert_eq!(config.cache_ttl, 3600);
563        assert!(config.edge_compression);
564        assert!(config.validate().is_ok());
565    }
566
567    #[test]
568    fn test_cache_control_headers() {
569        let immutable = CacheControl::immutable();
570        assert!(immutable.to_header().contains("public"));
571        assert!(immutable.to_header().contains("max-age=31536000"));
572
573        let no_cache = CacheControl::no_cache();
574        assert!(no_cache.to_header().contains("no-cache"));
575        assert!(no_cache.to_header().contains("no-store"));
576    }
577
578    #[test]
579    fn test_edge_node_scoring() {
580        let node = EdgeNode {
581            id: "edge1".to_string(),
582            location: "New York".to_string(),
583            region: CdnRegion::NorthAmerica,
584            status: EdgeNodeStatus::Active,
585            load: 50,
586            latency_ms: 50,
587            bandwidth_mbps: 1000,
588        };
589
590        let score = node.calculate_score();
591        assert!(score > 0.0 && score <= 1.0);
592        assert!(node.is_healthy());
593        assert!(node.is_available());
594    }
595
596    #[test]
597    fn test_cdn_manager() {
598        let mut manager = CdnManager::new(CdnConfig::default());
599
600        let node = EdgeNode::new("edge1".to_string(), "London".to_string(), CdnRegion::Europe);
601        manager.add_edge_node(node);
602
603        let best = manager.get_best_node(&CdnRegion::Europe);
604        assert!(best.is_some());
605        assert_eq!(best.unwrap().id, "edge1");
606    }
607
608    #[test]
609    fn test_package_upload() {
610        let mut manager = CdnManager::new(CdnConfig::default());
611
612        let data = b"package data";
613        let url = manager
614            .upload_package("test-package", "1.0.0", data)
615            .unwrap();
616
617        assert!(url.contains("test-package"));
618        assert!(url.contains("1.0.0"));
619
620        let retrieved_url = manager.get_package_url("test-package", "1.0.0");
621        assert_eq!(retrieved_url, Some(url));
622    }
623
624    #[test]
625    fn test_cache_purge() {
626        let mut manager = CdnManager::new(CdnConfig::default());
627
628        manager.upload_package("pkg1", "1.0.0", b"data1").unwrap();
629        manager.upload_package("pkg1", "2.0.0", b"data2").unwrap();
630
631        // Purge specific version
632        manager.purge_cache("pkg1", Some("1.0.0")).unwrap();
633        assert!(manager.get_package_url("pkg1", "1.0.0").is_none());
634        assert!(manager.get_package_url("pkg1", "2.0.0").is_some());
635
636        // Purge all versions
637        manager.purge_cache("pkg1", None).unwrap();
638        assert!(manager.get_package_url("pkg1", "2.0.0").is_none());
639    }
640
641    #[test]
642    fn test_cdn_statistics() {
643        let mut stats = CdnStatistics::new();
644
645        stats.record_request("us-east", 1000, 50, true);
646        stats.record_request("us-east", 2000, 100, false);
647
648        assert_eq!(stats.total_requests, 2);
649        assert_eq!(stats.cache_hit_rate, 0.5);
650        assert_eq!(stats.avg_response_ms, 75.0);
651        assert_eq!(stats.bytes_transferred, 3000);
652    }
653}