use crate::packs::types::Pack;
use async_trait::async_trait;
use ggen_utils::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::Duration;
use tracing::{debug, info};
#[async_trait]
pub trait CloudDistribution: Send + Sync {
async fn cache_pack(&self, pack: &Pack) -> Result<CacheInfo>;
async fn download_from_cache(&self, pack_id: &str, local_path: &Path) -> Result<()>;
async fn get_cache_stats(&self, pack_id: &str) -> Result<CacheStats>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheInfo {
pub cdn_url: String,
pub cache_key: String,
pub ttl: Duration,
pub hit_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub hit_ratio: f64,
pub bandwidth_saved: u64,
pub avg_download_time_ms: u64,
}
pub struct InMemoryCDN {
cache: std::sync::Arc<tokio::sync::RwLock<std::collections::HashMap<String, Vec<u8>>>>,
stats: std::sync::Arc<tokio::sync::RwLock<std::collections::HashMap<String, CacheStats>>>,
}
impl InMemoryCDN {
pub fn new() -> Self {
Self {
cache: std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
stats: std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::HashMap::new())),
}
}
}
impl Default for InMemoryCDN {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl CloudDistribution for InMemoryCDN {
async fn cache_pack(&self, pack: &Pack) -> Result<CacheInfo> {
info!("Caching pack '{}' in CDN", pack.id);
let cache_key = format!("pack:{}:{}", pack.id, pack.version);
let pack_data = serde_json::to_vec(pack)
.map_err(|e| Error::new(&format!("Failed to serialize pack: {}", e)))?;
{
let mut cache = self.cache.write().await;
cache.insert(cache_key.clone(), pack_data);
}
{
let mut stats = self.stats.write().await;
stats.entry(cache_key.clone()).or_insert(CacheStats {
hits: 0,
misses: 0,
hit_ratio: 0.0,
bandwidth_saved: 0,
avg_download_time_ms: 0,
});
}
Ok(CacheInfo {
cdn_url: format!("https://cdn.ggen.io/packs/{}/{}", pack.id, pack.version),
cache_key,
ttl: Duration::from_secs(3600), hit_count: 0,
})
}
async fn download_from_cache(&self, pack_id: &str, local_path: &Path) -> Result<()> {
info!(
"Downloading pack '{}' from cache to {}",
pack_id,
local_path.display()
);
let cache_key = format!("pack:{}", pack_id);
let data = {
let cache = self.cache.read().await;
if let Some(data) = cache.get(&cache_key) {
data.clone()
} else {
let matches: Vec<_> = cache
.iter()
.filter(|(k, _)| k.starts_with(&format!("pack:{}:", pack_id)))
.collect();
if let Some((_, data)) = matches.first() {
(*data).clone()
} else {
return Err(Error::new(&format!(
"Pack '{}' not found in cache",
pack_id
)));
}
}
};
{
let mut stats = self.stats.write().await;
if let Some(stat) = stats.get_mut(&cache_key) {
stat.hits += 1;
stat.hit_ratio = stat.hits as f64 / (stat.hits + stat.misses) as f64;
}
}
tokio::fs::create_dir_all(local_path.parent().unwrap_or(Path::new("."))).await?;
tokio::fs::write(local_path, data).await?;
debug!("Downloaded {} to {}", cache_key, local_path.display());
Ok(())
}
async fn get_cache_stats(&self, pack_id: &str) -> Result<CacheStats> {
let cache_key = format!("pack:{}", pack_id);
let stats = self.stats.read().await;
stats
.get(&cache_key)
.cloned()
.ok_or_else(|| Error::new(&format!("No cache stats for pack '{}'", pack_id)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packs::types::PackMetadata;
use std::collections::HashMap;
fn create_test_pack() -> Pack {
Pack {
id: "test-pack".to_string(),
name: "Test Pack".to_string(),
version: "1.0.0".to_string(),
description: "Test".to_string(),
category: "test".to_string(),
author: None,
repository: None,
license: None,
packages: vec![],
templates: vec![],
sparql_queries: HashMap::new(),
dependencies: vec![],
tags: vec![],
keywords: vec![],
production_ready: true,
metadata: PackMetadata::default(),
}
}
#[tokio::test]
async fn test_cache_pack() {
let cdn = InMemoryCDN::new();
let pack = create_test_pack();
let result = cdn.cache_pack(&pack).await;
assert!(result.is_ok());
let info = result.unwrap();
assert!(info.cdn_url.contains("test-pack"));
assert!(info.cdn_url.contains("1.0.0"));
}
#[tokio::test]
async fn test_download_from_cache() {
let cdn = InMemoryCDN::new();
let pack = create_test_pack();
cdn.cache_pack(&pack).await.unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let download_path = temp_dir.path().join("pack.json");
let result = cdn.download_from_cache("test-pack", &download_path).await;
assert!(result.is_ok());
assert!(download_path.exists());
}
#[tokio::test]
async fn test_download_nonexistent_pack() {
let cdn = InMemoryCDN::new();
let temp_dir = tempfile::tempdir().unwrap();
let download_path = temp_dir.path().join("pack.json");
let result = cdn.download_from_cache("nonexistent", &download_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_cache_stats() {
let cdn = InMemoryCDN::new();
let pack = create_test_pack();
cdn.cache_pack(&pack).await.unwrap();
let temp_dir = tempfile::tempdir().unwrap();
cdn.download_from_cache("test-pack", &temp_dir.path().join("pack1.json"))
.await
.unwrap();
assert!(temp_dir.path().join("pack1.json").exists());
}
}