1use std::collections::HashMap;
7use std::time::{Duration, SystemTime};
8
9use serde::{Deserialize, Serialize};
10use torsh_core::error::{Result, TorshError};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub enum CdnProvider {
15 Cloudflare,
17 CloudFront,
19 GoogleCdn,
21 AzureCdn,
23 Fastly,
25 Custom(String),
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CdnConfig {
32 pub provider: CdnProvider,
34 pub endpoint: String,
36 pub api_key: Option<String>,
38 pub cache_ttl: u64,
40 pub edge_compression: bool,
42 pub regions: Vec<CdnRegion>,
44 pub custom_headers: HashMap<String, String>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub enum CdnRegion {
51 NorthAmerica,
53 Europe,
55 AsiaPacific,
57 SouthAmerica,
59 Africa,
61 MiddleEast,
63 Oceania,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CacheControl {
70 pub max_age: u64,
72 pub public: bool,
74 pub private: bool,
76 pub no_cache: bool,
78 pub no_store: bool,
80 pub must_revalidate: bool,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct EdgeNode {
87 pub id: String,
89 pub location: String,
91 pub region: CdnRegion,
93 pub status: EdgeNodeStatus,
95 pub load: u8,
97 pub latency_ms: u64,
99 pub bandwidth_mbps: u64,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub enum EdgeNodeStatus {
106 Active,
108 Degraded,
110 Offline,
112 Maintenance,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct CdnStatistics {
119 pub total_requests: u64,
121 pub cache_hit_rate: f64,
123 pub bytes_transferred: u64,
125 pub avg_response_ms: f64,
127 pub requests_by_region: HashMap<String, u64>,
129 pub error_rate: f64,
131}
132
133pub struct CdnManager {
135 config: CdnConfig,
137 edge_nodes: Vec<EdgeNode>,
139 statistics: CdnStatistics,
141 cache: HashMap<String, CachedItem>,
143}
144
145#[derive(Debug, Clone)]
147struct CachedItem {
148 _key: String,
150 url: String,
152 expires_at: SystemTime,
154 _size: u64,
156 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, 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 pub fn new(provider: CdnProvider, endpoint: String) -> Self {
181 Self {
182 provider,
183 endpoint,
184 ..Default::default()
185 }
186 }
187
188 pub fn with_api_key(mut self, api_key: String) -> Self {
190 self.api_key = Some(api_key);
191 self
192 }
193
194 pub fn with_cache_ttl(mut self, ttl: u64) -> Self {
196 self.cache_ttl = ttl;
197 self
198 }
199
200 pub fn with_edge_compression(mut self, enabled: bool) -> Self {
202 self.edge_compression = enabled;
203 self
204 }
205
206 pub fn add_region(mut self, region: CdnRegion) -> Self {
208 if !self.regions.contains(®ion) {
209 self.regions.push(region);
210 }
211 self
212 }
213
214 pub fn add_header(mut self, key: String, value: String) -> Self {
216 self.custom_headers.insert(key, value);
217 self
218 }
219
220 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, public: true,
249 private: false,
250 no_cache: false,
251 no_store: false,
252 must_revalidate: false,
253 }
254 }
255}
256
257impl CacheControl {
258 pub fn immutable() -> Self {
260 Self {
261 max_age: 31536000, public: true,
263 private: false,
264 no_cache: false,
265 no_store: false,
266 must_revalidate: false,
267 }
268 }
269
270 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 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 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, }
321 }
322
323 pub fn is_healthy(&self) -> bool {
325 matches!(self.status, EdgeNodeStatus::Active) && self.load < 90
326 }
327
328 pub fn is_available(&self) -> bool {
330 matches!(
331 self.status,
332 EdgeNodeStatus::Active | EdgeNodeStatus::Degraded
333 )
334 }
335
336 pub fn calculate_score(&self) -> f64 {
338 if !self.is_available() {
339 return 0.0;
340 }
341
342 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 (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 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 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 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 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 *self
388 .requests_by_region
389 .entry(region.to_string())
390 .or_insert(0) += 1;
391 }
392
393 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 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 pub fn add_edge_node(&mut self, node: EdgeNode) {
420 self.edge_nodes.push(node);
421 }
422
423 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 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 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 let url = format!(
461 "{}/packages/{}/{}",
462 self.config.endpoint, package_name, version
463 );
464
465 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 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 if SystemTime::now() < item.expires_at {
487 item.hits += 1;
488 return Some(item.url.clone());
489 } else {
490 self.cache.remove(&cache_key);
492 }
493 }
494
495 None
496 }
497
498 pub fn purge_cache(&mut self, package_name: &str, version: Option<&str>) -> Result<()> {
500 if let Some(ver) = version {
501 let cache_key = format!("{}/{}", package_name, ver);
503 self.cache.remove(&cache_key);
504 } else {
505 let prefix = format!("{}/", package_name);
507 self.cache.retain(|k, _| !k.starts_with(&prefix));
508 }
509
510 Ok(())
511 }
512
513 pub fn get_statistics(&self) -> &CdnStatistics {
515 &self.statistics
516 }
517
518 pub fn get_cache_hit_rate(&self) -> f64 {
520 self.statistics.cache_hit_rate
521 }
522
523 pub fn get_healthy_nodes(&self) -> Vec<&EdgeNode> {
525 self.edge_nodes.iter().filter(|n| n.is_healthy()).collect()
526 }
527
528 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 pub fn generate_cache_control(&self, package_version: &str) -> String {
538 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 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 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}