use std::collections::HashMap;
use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use torsh_core::error::{Result, TorshError};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CdnProvider {
Cloudflare,
CloudFront,
GoogleCdn,
AzureCdn,
Fastly,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CdnConfig {
pub provider: CdnProvider,
pub endpoint: String,
pub api_key: Option<String>,
pub cache_ttl: u64,
pub edge_compression: bool,
pub regions: Vec<CdnRegion>,
pub custom_headers: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CdnRegion {
NorthAmerica,
Europe,
AsiaPacific,
SouthAmerica,
Africa,
MiddleEast,
Oceania,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheControl {
pub max_age: u64,
pub public: bool,
pub private: bool,
pub no_cache: bool,
pub no_store: bool,
pub must_revalidate: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeNode {
pub id: String,
pub location: String,
pub region: CdnRegion,
pub status: EdgeNodeStatus,
pub load: u8,
pub latency_ms: u64,
pub bandwidth_mbps: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EdgeNodeStatus {
Active,
Degraded,
Offline,
Maintenance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CdnStatistics {
pub total_requests: u64,
pub cache_hit_rate: f64,
pub bytes_transferred: u64,
pub avg_response_ms: f64,
pub requests_by_region: HashMap<String, u64>,
pub error_rate: f64,
}
pub struct CdnManager {
config: CdnConfig,
edge_nodes: Vec<EdgeNode>,
statistics: CdnStatistics,
cache: HashMap<String, CachedItem>,
}
#[derive(Debug, Clone)]
struct CachedItem {
_key: String,
url: String,
expires_at: SystemTime,
_size: u64,
hits: u64,
}
impl Default for CdnConfig {
fn default() -> Self {
Self {
provider: CdnProvider::Cloudflare,
endpoint: "https://cdn.torsh.rs".to_string(),
api_key: None,
cache_ttl: 86400, edge_compression: true,
regions: vec![
CdnRegion::NorthAmerica,
CdnRegion::Europe,
CdnRegion::AsiaPacific,
],
custom_headers: HashMap::new(),
}
}
}
impl CdnConfig {
pub fn new(provider: CdnProvider, endpoint: String) -> Self {
Self {
provider,
endpoint,
..Default::default()
}
}
pub fn with_api_key(mut self, api_key: String) -> Self {
self.api_key = Some(api_key);
self
}
pub fn with_cache_ttl(mut self, ttl: u64) -> Self {
self.cache_ttl = ttl;
self
}
pub fn with_edge_compression(mut self, enabled: bool) -> Self {
self.edge_compression = enabled;
self
}
pub fn add_region(mut self, region: CdnRegion) -> Self {
if !self.regions.contains(®ion) {
self.regions.push(region);
}
self
}
pub fn add_header(mut self, key: String, value: String) -> Self {
self.custom_headers.insert(key, value);
self
}
pub fn validate(&self) -> Result<()> {
if self.endpoint.is_empty() {
return Err(TorshError::InvalidArgument(
"CDN endpoint cannot be empty".to_string(),
));
}
if self.regions.is_empty() {
return Err(TorshError::InvalidArgument(
"At least one region must be configured".to_string(),
));
}
if self.cache_ttl == 0 {
return Err(TorshError::InvalidArgument(
"Cache TTL must be greater than zero".to_string(),
));
}
Ok(())
}
}
impl Default for CacheControl {
fn default() -> Self {
Self {
max_age: 86400, public: true,
private: false,
no_cache: false,
no_store: false,
must_revalidate: false,
}
}
}
impl CacheControl {
pub fn immutable() -> Self {
Self {
max_age: 31536000, public: true,
private: false,
no_cache: false,
no_store: false,
must_revalidate: false,
}
}
pub fn no_cache() -> Self {
Self {
max_age: 0,
public: false,
private: false,
no_cache: true,
no_store: true,
must_revalidate: true,
}
}
pub fn to_header(&self) -> String {
let mut parts = Vec::new();
if self.public {
parts.push("public".to_string());
}
if self.private {
parts.push("private".to_string());
}
if self.no_cache {
parts.push("no-cache".to_string());
}
if self.no_store {
parts.push("no-store".to_string());
}
if self.must_revalidate {
parts.push("must-revalidate".to_string());
}
if self.max_age > 0 {
parts.push(format!("max-age={}", self.max_age));
}
parts.join(", ")
}
}
impl EdgeNode {
pub fn new(id: String, location: String, region: CdnRegion) -> Self {
Self {
id,
location,
region,
status: EdgeNodeStatus::Active,
load: 0,
latency_ms: 0,
bandwidth_mbps: 1000, }
}
pub fn is_healthy(&self) -> bool {
matches!(self.status, EdgeNodeStatus::Active) && self.load < 90
}
pub fn is_available(&self) -> bool {
matches!(
self.status,
EdgeNodeStatus::Active | EdgeNodeStatus::Degraded
)
}
pub fn calculate_score(&self) -> f64 {
if !self.is_available() {
return 0.0;
}
let latency_score = 1.0 / (1.0 + self.latency_ms as f64 / 100.0);
let load_score = 1.0 - (self.load as f64 / 100.0);
let bandwidth_score = (self.bandwidth_mbps as f64).min(10000.0) / 10000.0;
(latency_score * 0.4) + (load_score * 0.4) + (bandwidth_score * 0.2)
}
}
impl Default for CdnStatistics {
fn default() -> Self {
Self::new()
}
}
impl CdnStatistics {
pub fn new() -> Self {
Self {
total_requests: 0,
cache_hit_rate: 0.0,
bytes_transferred: 0,
avg_response_ms: 0.0,
requests_by_region: HashMap::new(),
error_rate: 0.0,
}
}
pub fn record_request(&mut self, region: &str, bytes: u64, response_ms: u64, cache_hit: bool) {
self.total_requests += 1;
self.bytes_transferred += bytes;
let hit_value = if cache_hit { 1.0 } else { 0.0 };
self.cache_hit_rate = (self.cache_hit_rate * (self.total_requests - 1) as f64 + hit_value)
/ self.total_requests as f64;
self.avg_response_ms = (self.avg_response_ms * (self.total_requests - 1) as f64
+ response_ms as f64)
/ self.total_requests as f64;
*self
.requests_by_region
.entry(region.to_string())
.or_insert(0) += 1;
}
pub fn record_error(&mut self) {
self.total_requests += 1;
self.error_rate =
(self.error_rate * (self.total_requests - 1) as f64 + 1.0) / self.total_requests as f64;
}
}
impl Default for CdnManager {
fn default() -> Self {
Self::new(CdnConfig::default())
}
}
impl CdnManager {
pub fn new(config: CdnConfig) -> Self {
Self {
config,
edge_nodes: Vec::new(),
statistics: CdnStatistics::new(),
cache: HashMap::new(),
}
}
pub fn add_edge_node(&mut self, node: EdgeNode) {
self.edge_nodes.push(node);
}
pub fn get_best_node(&self, region: &CdnRegion) -> Option<&EdgeNode> {
let mut candidates: Vec<_> = self
.edge_nodes
.iter()
.filter(|n| n.is_available() && &n.region == region)
.collect();
if candidates.is_empty() {
candidates = self
.edge_nodes
.iter()
.filter(|n| n.is_available())
.collect();
}
candidates
.iter()
.max_by(|a, b| {
a.calculate_score()
.partial_cmp(&b.calculate_score())
.unwrap_or(std::cmp::Ordering::Equal)
})
.copied()
}
pub fn upload_package(
&mut self,
package_name: &str,
version: &str,
_data: &[u8],
) -> Result<String> {
let cache_key = format!("{}/{}", package_name, version);
let url = format!(
"{}/packages/{}/{}",
self.config.endpoint, package_name, version
);
let cache_item = CachedItem {
_key: cache_key.clone(),
url: url.clone(),
expires_at: SystemTime::now() + Duration::from_secs(self.config.cache_ttl),
_size: _data.len() as u64,
hits: 0,
};
self.cache.insert(cache_key, cache_item);
Ok(url)
}
pub fn get_package_url(&mut self, package_name: &str, version: &str) -> Option<String> {
let cache_key = format!("{}/{}", package_name, version);
if let Some(item) = self.cache.get_mut(&cache_key) {
if SystemTime::now() < item.expires_at {
item.hits += 1;
return Some(item.url.clone());
} else {
self.cache.remove(&cache_key);
}
}
None
}
pub fn purge_cache(&mut self, package_name: &str, version: Option<&str>) -> Result<()> {
if let Some(ver) = version {
let cache_key = format!("{}/{}", package_name, ver);
self.cache.remove(&cache_key);
} else {
let prefix = format!("{}/", package_name);
self.cache.retain(|k, _| !k.starts_with(&prefix));
}
Ok(())
}
pub fn get_statistics(&self) -> &CdnStatistics {
&self.statistics
}
pub fn get_cache_hit_rate(&self) -> f64 {
self.statistics.cache_hit_rate
}
pub fn get_healthy_nodes(&self) -> Vec<&EdgeNode> {
self.edge_nodes.iter().filter(|n| n.is_healthy()).collect()
}
pub fn get_nodes_by_region(&self, region: &CdnRegion) -> Vec<&EdgeNode> {
self.edge_nodes
.iter()
.filter(|n| &n.region == region)
.collect()
}
pub fn generate_cache_control(&self, package_version: &str) -> String {
if !package_version.is_empty() {
CacheControl::immutable().to_header()
} else {
CacheControl::default().to_header()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cdn_config() {
let config = CdnConfig::new(
CdnProvider::Cloudflare,
"https://cdn.example.com".to_string(),
)
.with_cache_ttl(3600)
.with_edge_compression(true)
.add_region(CdnRegion::NorthAmerica);
assert_eq!(config.provider, CdnProvider::Cloudflare);
assert_eq!(config.cache_ttl, 3600);
assert!(config.edge_compression);
assert!(config.validate().is_ok());
}
#[test]
fn test_cache_control_headers() {
let immutable = CacheControl::immutable();
assert!(immutable.to_header().contains("public"));
assert!(immutable.to_header().contains("max-age=31536000"));
let no_cache = CacheControl::no_cache();
assert!(no_cache.to_header().contains("no-cache"));
assert!(no_cache.to_header().contains("no-store"));
}
#[test]
fn test_edge_node_scoring() {
let node = EdgeNode {
id: "edge1".to_string(),
location: "New York".to_string(),
region: CdnRegion::NorthAmerica,
status: EdgeNodeStatus::Active,
load: 50,
latency_ms: 50,
bandwidth_mbps: 1000,
};
let score = node.calculate_score();
assert!(score > 0.0 && score <= 1.0);
assert!(node.is_healthy());
assert!(node.is_available());
}
#[test]
fn test_cdn_manager() {
let mut manager = CdnManager::new(CdnConfig::default());
let node = EdgeNode::new("edge1".to_string(), "London".to_string(), CdnRegion::Europe);
manager.add_edge_node(node);
let best = manager.get_best_node(&CdnRegion::Europe);
assert!(best.is_some());
assert_eq!(best.unwrap().id, "edge1");
}
#[test]
fn test_package_upload() {
let mut manager = CdnManager::new(CdnConfig::default());
let data = b"package data";
let url = manager
.upload_package("test-package", "1.0.0", data)
.unwrap();
assert!(url.contains("test-package"));
assert!(url.contains("1.0.0"));
let retrieved_url = manager.get_package_url("test-package", "1.0.0");
assert_eq!(retrieved_url, Some(url));
}
#[test]
fn test_cache_purge() {
let mut manager = CdnManager::new(CdnConfig::default());
manager.upload_package("pkg1", "1.0.0", b"data1").unwrap();
manager.upload_package("pkg1", "2.0.0", b"data2").unwrap();
manager.purge_cache("pkg1", Some("1.0.0")).unwrap();
assert!(manager.get_package_url("pkg1", "1.0.0").is_none());
assert!(manager.get_package_url("pkg1", "2.0.0").is_some());
manager.purge_cache("pkg1", None).unwrap();
assert!(manager.get_package_url("pkg1", "2.0.0").is_none());
}
#[test]
fn test_cdn_statistics() {
let mut stats = CdnStatistics::new();
stats.record_request("us-east", 1000, 50, true);
stats.record_request("us-east", 2000, 100, false);
assert_eq!(stats.total_requests, 2);
assert_eq!(stats.cache_hit_rate, 0.5);
assert_eq!(stats.avg_response_ms, 75.0);
assert_eq!(stats.bytes_transferred, 3000);
}
}