1use bytes::Bytes;
49use reqwest::Response;
50use std::path::PathBuf;
51use tokio::io::AsyncRead;
52use tracing::debug;
53
54use ngdp_cdn::CdnClient;
55
56use crate::{CdnCache, Result};
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60enum ContentType {
61 Config,
62 Data,
63 Patch,
64}
65
66impl ContentType {
67 fn from_path(path: &str) -> Self {
69 let path_lower = path.to_lowercase();
70 if path_lower.contains("/config") || path_lower.ends_with("config") {
71 Self::Config
72 } else if path_lower.contains("/patch") || path_lower.ends_with("patch") {
73 Self::Patch
74 } else {
75 Self::Data
76 }
77 }
78}
79
80pub struct CachedCdnClient {
82 client: CdnClient,
84 cache_base_dir: PathBuf,
86 enabled: bool,
88}
89
90impl CachedCdnClient {
91 pub async fn new() -> Result<Self> {
93 let client = CdnClient::new()?;
94 let cache_base_dir = crate::get_cache_dir()?.join("cdn");
95 crate::ensure_dir(&cache_base_dir).await?;
96
97 debug!("Initialized cached CDN client");
98
99 Ok(Self {
100 client,
101 cache_base_dir,
102 enabled: true,
103 })
104 }
105
106 pub async fn for_product(product: &str) -> Result<Self> {
108 let client = CdnClient::new()?;
109 let cache_base_dir = crate::get_cache_dir()?.join("cdn").join(product);
110 crate::ensure_dir(&cache_base_dir).await?;
111
112 debug!("Initialized cached CDN client for product '{}'", product);
113
114 Ok(Self {
115 client,
116 cache_base_dir,
117 enabled: true,
118 })
119 }
120
121 pub async fn with_cache_dir(cache_dir: PathBuf) -> Result<Self> {
123 let client = CdnClient::new()?;
124 crate::ensure_dir(&cache_dir).await?;
125
126 Ok(Self {
127 client,
128 cache_base_dir: cache_dir,
129 enabled: true,
130 })
131 }
132
133 pub async fn with_client(client: CdnClient) -> Result<Self> {
135 let cache_base_dir = crate::get_cache_dir()?.join("cdn");
136 crate::ensure_dir(&cache_base_dir).await?;
137
138 Ok(Self {
139 client,
140 cache_base_dir,
141 enabled: true,
142 })
143 }
144
145 pub fn add_primary_host(&self, host: impl Into<String>) {
147 self.client.add_primary_host(host);
148 }
149
150 pub fn add_primary_hosts(&self, hosts: impl IntoIterator<Item = impl Into<String>>) {
152 self.client.add_primary_hosts(hosts);
153 }
154
155 pub fn add_fallback_host(&self, host: impl Into<String>) {
157 self.client.add_fallback_host(host);
158 }
159
160 pub fn add_fallback_hosts(&self, hosts: impl IntoIterator<Item = impl Into<String>>) {
162 self.client.add_fallback_hosts(hosts);
163 }
164
165 pub fn set_primary_hosts(&self, hosts: impl IntoIterator<Item = impl Into<String>>) {
167 self.client.set_primary_hosts(hosts);
168 }
169
170 pub fn get_all_hosts(&self) -> Vec<String> {
172 self.client.get_all_hosts()
173 }
174
175 pub fn set_caching_enabled(&mut self, enabled: bool) {
177 self.enabled = enabled;
178 }
179
180 pub fn cache_dir(&self) -> &PathBuf {
182 &self.cache_base_dir
183 }
184
185 async fn get_cache_for_path(&self, cdn_path: &str) -> Result<CdnCache> {
187 let mut cache = CdnCache::with_base_dir(self.cache_base_dir.clone()).await?;
189 cache.set_cdn_path(Some(cdn_path.to_string()));
190 Ok(cache)
191 }
192
193 async fn is_cached(&self, path: &str, hash: &str) -> Result<bool> {
195 let cache = self.get_cache_for_path(path).await?;
196 let content_type = ContentType::from_path(path);
197 Ok(match content_type {
198 ContentType::Config => cache.has_config(hash).await,
199 ContentType::Data => cache.has_data(hash).await,
200 ContentType::Patch => cache.has_patch(hash).await,
201 })
202 }
203
204 async fn read_from_cache(&self, path: &str, hash: &str) -> Result<Bytes> {
206 let cache = self.get_cache_for_path(path).await?;
207 let content_type = ContentType::from_path(path);
208 let data = match content_type {
209 ContentType::Config => cache.read_config(hash).await?,
210 ContentType::Data => cache.read_data(hash).await?,
211 ContentType::Patch => cache.read_patch(hash).await?,
212 };
213 Ok(Bytes::from(data))
214 }
215
216 async fn write_to_cache(&self, path: &str, hash: &str, data: &[u8]) -> Result<()> {
218 let cache = self.get_cache_for_path(path).await?;
219 let content_type = ContentType::from_path(path);
220 match content_type {
221 ContentType::Config => cache.write_config(hash, data).await?,
222 ContentType::Data => cache.write_data(hash, data).await?,
223 ContentType::Patch => cache.write_patch(hash, data).await?,
224 };
225 Ok(())
226 }
227
228 pub async fn request(&self, url: &str) -> Result<Response> {
233 Ok(self.client.request(url).await?)
234 }
235
236 pub async fn download(&self, cdn_host: &str, path: &str, hash: &str) -> Result<CachedResponse> {
242 if self.enabled && self.is_cached(path, hash).await? {
244 debug!("Cache hit for CDN {}/{}", path, hash);
245 let data = self.read_from_cache(path, hash).await?;
246 return Ok(CachedResponse::from_cache(data));
247 }
248
249 debug!("Cache miss for CDN {}/{}, fetching from server", path, hash);
251 let response = self.client.download(cdn_host, path, hash).await?;
252
253 let data = response.bytes().await?;
255
256 if self.enabled {
258 if let Err(e) = self.write_to_cache(path, hash, &data).await {
259 debug!("Failed to write to CDN cache: {}", e);
260 }
261 }
262
263 Ok(CachedResponse::from_network(data))
264 }
265
266 pub async fn download_stream(
272 &self,
273 cdn_host: &str,
274 path: &str,
275 hash: &str,
276 ) -> Result<Box<dyn AsyncRead + Unpin + Send>> {
277 if ContentType::from_path(path) == ContentType::Data {
279 let cache = self.get_cache_for_path(path).await?;
281 if self.enabled && cache.has_data(hash).await {
282 debug!("Cache hit for CDN {}/{} (streaming)", path, hash);
283 let file = cache.open_data(hash).await?;
284 return Ok(Box::new(file));
285 }
286 }
287
288 let response = self.download(cdn_host, path, hash).await?;
290 let data = response.bytes().await?;
291
292 Ok(Box::new(std::io::Cursor::new(data.to_vec())))
294 }
295
296 pub async fn cached_size(&self, path: &str, hash: &str) -> Result<Option<u64>> {
298 if !self.enabled || !self.is_cached(path, hash).await? {
299 return Ok(None);
300 }
301
302 if ContentType::from_path(path) == ContentType::Data {
304 let cache = self.get_cache_for_path(path).await?;
305 Ok(Some(cache.data_size(hash).await?))
306 } else {
307 let data = self.read_from_cache(path, hash).await?;
309 Ok(Some(data.len() as u64))
310 }
311 }
312
313 pub async fn clear_cache(&self) -> Result<()> {
318 if tokio::fs::metadata(&self.cache_base_dir).await.is_ok() {
319 tokio::fs::remove_dir_all(&self.cache_base_dir).await?;
320 }
321 Ok(())
322 }
323
324 pub async fn cache_stats(&self) -> Result<CacheStats> {
326 let mut stats = CacheStats::default();
327
328 for entry in walkdir::WalkDir::new(&self.cache_base_dir)
330 .into_iter()
331 .flatten()
332 {
333 if entry.file_type().is_file() {
334 if let Ok(metadata) = entry.metadata() {
335 stats.total_files += 1;
336 stats.total_size += metadata.len();
337
338 let path = entry.path();
339 if path.to_string_lossy().contains("config") {
340 stats.config_files += 1;
341 stats.config_size += metadata.len();
342 } else if path.to_string_lossy().contains("patch") {
343 stats.patch_files += 1;
344 stats.patch_size += metadata.len();
345 } else if path.to_string_lossy().contains("data") {
346 stats.data_files += 1;
347 stats.data_size += metadata.len();
348 }
349 }
350 }
351 }
352
353 Ok(stats)
354 }
355
356 pub async fn download_build_config(
360 &self,
361 cdn_host: &str,
362 path: &str,
363 hash: &str,
364 ) -> Result<CachedResponse> {
365 let config_path = format!("{}/config", path.trim_end_matches('/'));
366 self.download(cdn_host, &config_path, hash).await
367 }
368
369 pub async fn download_cdn_config(
373 &self,
374 cdn_host: &str,
375 path: &str,
376 hash: &str,
377 ) -> Result<CachedResponse> {
378 let config_path = format!("{}/config", path.trim_end_matches('/'));
379 self.download(cdn_host, &config_path, hash).await
380 }
381
382 pub async fn download_product_config(
387 &self,
388 cdn_host: &str,
389 config_path: &str,
390 hash: &str,
391 ) -> Result<CachedResponse> {
392 self.download(cdn_host, config_path, hash).await
393 }
394
395 pub async fn download_key_ring(
399 &self,
400 cdn_host: &str,
401 path: &str,
402 hash: &str,
403 ) -> Result<CachedResponse> {
404 let config_path = format!("{}/config", path.trim_end_matches('/'));
405 self.download(cdn_host, &config_path, hash).await
406 }
407
408 pub async fn download_data(
412 &self,
413 cdn_host: &str,
414 path: &str,
415 hash: &str,
416 ) -> Result<CachedResponse> {
417 let data_path = format!("{}/data", path.trim_end_matches('/'));
418 self.download(cdn_host, &data_path, hash).await
419 }
420
421 pub async fn download_patch(
425 &self,
426 cdn_host: &str,
427 path: &str,
428 hash: &str,
429 ) -> Result<CachedResponse> {
430 let patch_path = format!("{}/patch", path.trim_end_matches('/'));
431 self.download(cdn_host, &patch_path, hash).await
432 }
433}
434
435pub struct CachedResponse {
437 data: Bytes,
439 from_cache: bool,
441}
442
443impl CachedResponse {
444 fn from_cache(data: Bytes) -> Self {
446 Self {
447 data,
448 from_cache: true,
449 }
450 }
451
452 fn from_network(data: Bytes) -> Self {
454 Self {
455 data,
456 from_cache: false,
457 }
458 }
459
460 pub fn is_from_cache(&self) -> bool {
462 self.from_cache
463 }
464
465 pub async fn bytes(self) -> Result<Bytes> {
467 Ok(self.data)
468 }
469
470 pub async fn text(self) -> Result<String> {
472 Ok(String::from_utf8(self.data.to_vec())?)
473 }
474
475 pub fn content_length(&self) -> usize {
477 self.data.len()
478 }
479}
480
481#[derive(Debug, Default, Clone)]
483pub struct CacheStats {
484 pub total_files: u64,
486 pub total_size: u64,
488 pub config_files: u64,
490 pub config_size: u64,
492 pub data_files: u64,
494 pub data_size: u64,
496 pub patch_files: u64,
498 pub patch_size: u64,
500}
501
502impl CacheStats {
503 pub fn total_size_human(&self) -> String {
505 format_bytes(self.total_size)
506 }
507
508 pub fn config_size_human(&self) -> String {
510 format_bytes(self.config_size)
511 }
512
513 pub fn data_size_human(&self) -> String {
515 format_bytes(self.data_size)
516 }
517
518 pub fn patch_size_human(&self) -> String {
520 format_bytes(self.patch_size)
521 }
522}
523
524fn format_bytes(bytes: u64) -> String {
526 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
527 let mut size = bytes as f64;
528 let mut unit_idx = 0;
529
530 while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
531 size /= 1024.0;
532 unit_idx += 1;
533 }
534
535 if unit_idx == 0 {
536 format!("{} {}", size as u64, UNITS[unit_idx])
537 } else {
538 format!("{:.2} {}", size, UNITS[unit_idx])
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use tempfile::TempDir;
546
547 #[tokio::test]
548 async fn test_cached_cdn_client_creation() {
549 let client = CachedCdnClient::new().await.unwrap();
550 assert!(client.enabled);
551 }
552
553 #[tokio::test]
554 async fn test_content_type_detection() {
555 assert_eq!(
556 ContentType::from_path("tpr/configs/data/config"),
557 ContentType::Config
558 );
559 assert_eq!(
560 ContentType::from_path("tpr/wow/config"),
561 ContentType::Config
562 );
563 assert_eq!(ContentType::from_path("config"), ContentType::Config);
564 assert_eq!(ContentType::from_path("tpr/wow/data"), ContentType::Data);
565 assert_eq!(ContentType::from_path("tpr/wow/patch"), ContentType::Patch);
566 assert_eq!(ContentType::from_path("tpr/wow"), ContentType::Data);
567 }
568
569 #[tokio::test]
570 async fn test_cache_enabling() {
571 let mut client = CachedCdnClient::new().await.unwrap();
572
573 client.set_caching_enabled(false);
574 assert!(!client.enabled);
575
576 client.set_caching_enabled(true);
577 assert!(client.enabled);
578 }
579
580 #[tokio::test]
581 async fn test_format_bytes() {
582 assert_eq!(format_bytes(0), "0 B");
583 assert_eq!(format_bytes(1023), "1023 B");
584 assert_eq!(format_bytes(1024), "1.00 KB");
585 assert_eq!(format_bytes(1536), "1.50 KB");
586 assert_eq!(format_bytes(1048576), "1.00 MB");
587 assert_eq!(format_bytes(1073741824), "1.00 GB");
588 }
589
590 #[tokio::test]
591 async fn test_cache_with_temp_dir() {
592 let temp_dir = TempDir::new().unwrap();
593 let client = CachedCdnClient::with_cache_dir(temp_dir.path().to_path_buf())
594 .await
595 .unwrap();
596
597 assert_eq!(client.cache_dir(), &temp_dir.path().to_path_buf());
598 }
599}