ngdp_cache/
cdn.rs

1//! CDN content cache implementation
2//!
3//! This module caches all CDN content following the CDN path structure:
4//! - `{cdn_path}/config/{first2}/{next2}/{hash}` - Configuration files
5//! - `{cdn_path}/data/{first2}/{next2}/{hash}` - Data files and archives
6//! - `{cdn_path}/patch/{first2}/{next2}/{hash}` - Patch files
7//!
8//! Where `{cdn_path}` is the path provided by the CDN (e.g., "tpr/wow").
9//! Archives and indices are stored in the data directory with `.index` extension for indices.
10
11use std::path::PathBuf;
12use tracing::{debug, trace};
13
14use crate::{Result, ensure_dir, get_cache_dir};
15
16/// Cache for CDN content following the standard CDN directory structure
17pub struct CdnCache {
18    /// Base directory for CDN cache
19    base_dir: PathBuf,
20    /// CDN path prefix (e.g., "tpr/wow")
21    cdn_path: Option<String>,
22}
23
24impl CdnCache {
25    /// Create a new CDN cache
26    pub async fn new() -> Result<Self> {
27        let base_dir = get_cache_dir()?.join("cdn");
28        ensure_dir(&base_dir).await?;
29
30        debug!("Initialized CDN cache at: {:?}", base_dir);
31
32        Ok(Self {
33            base_dir,
34            cdn_path: None,
35        })
36    }
37
38    /// Create a CDN cache for a specific product
39    pub async fn for_product(product: &str) -> Result<Self> {
40        let base_dir = get_cache_dir()?.join("cdn").join(product);
41        ensure_dir(&base_dir).await?;
42
43        debug!(
44            "Initialized CDN cache for product '{}' at: {:?}",
45            product, base_dir
46        );
47
48        Ok(Self {
49            base_dir,
50            cdn_path: None,
51        })
52    }
53
54    /// Create a CDN cache with a custom base directory
55    pub async fn with_base_dir(base_dir: PathBuf) -> Result<Self> {
56        ensure_dir(&base_dir).await?;
57
58        debug!("Initialized CDN cache at: {:?}", base_dir);
59
60        Ok(Self {
61            base_dir,
62            cdn_path: None,
63        })
64    }
65
66    /// Create a CDN cache with a specific CDN path
67    pub async fn with_cdn_path(cdn_path: &str) -> Result<Self> {
68        let base_dir = get_cache_dir()?.join("cdn");
69        ensure_dir(&base_dir).await?;
70
71        debug!(
72            "Initialized CDN cache with path '{}' at: {:?}",
73            cdn_path, base_dir
74        );
75
76        Ok(Self {
77            base_dir,
78            cdn_path: Some(cdn_path.to_string()),
79        })
80    }
81
82    /// Set the CDN path for this cache
83    pub fn set_cdn_path(&mut self, cdn_path: Option<String>) {
84        self.cdn_path = cdn_path;
85    }
86
87    /// Get the effective base directory including CDN path
88    fn effective_base_dir(&self) -> PathBuf {
89        if let Some(ref cdn_path) = self.cdn_path {
90            self.base_dir.join(cdn_path)
91        } else {
92            self.base_dir.clone()
93        }
94    }
95
96    /// Get the config cache directory
97    pub fn config_dir(&self) -> PathBuf {
98        let base = self.effective_base_dir();
99        let path_str = base.to_string_lossy();
100
101        // Check if the path already ends with "config" or contains "configs"
102        if path_str.ends_with("/config") || path_str.ends_with("\\config") {
103            // Path already has /config suffix, don't add another
104            base
105        } else if path_str.contains("configs/") || path_str.contains("configs\\") {
106            // For paths like "tpr/configs/data", don't add "config"
107            base
108        } else {
109            // For paths like "tpr/wow", add "config"
110            base.join("config")
111        }
112    }
113
114    /// Get the data cache directory
115    pub fn data_dir(&self) -> PathBuf {
116        self.effective_base_dir().join("data")
117    }
118
119    /// Get the patch cache directory
120    pub fn patch_dir(&self) -> PathBuf {
121        self.effective_base_dir().join("patch")
122    }
123
124    /// Construct a config cache path from a hash
125    ///
126    /// Follows CDN structure: config/{first2}/{next2}/{hash}
127    pub fn config_path(&self, hash: &str) -> PathBuf {
128        if hash.len() >= 4 {
129            self.config_dir()
130                .join(&hash[..2])
131                .join(&hash[2..4])
132                .join(hash)
133        } else {
134            self.config_dir().join(hash)
135        }
136    }
137
138    /// Construct a data cache path from a hash
139    ///
140    /// Follows CDN structure: data/{first2}/{next2}/{hash}
141    pub fn data_path(&self, hash: &str) -> PathBuf {
142        if hash.len() >= 4 {
143            self.data_dir()
144                .join(&hash[..2])
145                .join(&hash[2..4])
146                .join(hash)
147        } else {
148            self.data_dir().join(hash)
149        }
150    }
151
152    /// Construct a patch cache path from a hash
153    ///
154    /// Follows CDN structure: patch/{first2}/{next2}/{hash}
155    pub fn patch_path(&self, hash: &str) -> PathBuf {
156        if hash.len() >= 4 {
157            self.patch_dir()
158                .join(&hash[..2])
159                .join(&hash[2..4])
160                .join(hash)
161        } else {
162            self.patch_dir().join(hash)
163        }
164    }
165
166    /// Construct an index cache path from a hash
167    ///
168    /// Follows CDN structure: data/{first2}/{next2}/{hash}.index
169    pub fn index_path(&self, hash: &str) -> PathBuf {
170        let mut path = self.data_path(hash);
171        path.set_extension("index");
172        path
173    }
174
175    /// Check if a config exists in cache
176    pub async fn has_config(&self, hash: &str) -> bool {
177        tokio::fs::metadata(self.config_path(hash)).await.is_ok()
178    }
179
180    /// Check if data exists in cache
181    pub async fn has_data(&self, hash: &str) -> bool {
182        tokio::fs::metadata(self.data_path(hash)).await.is_ok()
183    }
184
185    /// Check if a patch exists in cache
186    pub async fn has_patch(&self, hash: &str) -> bool {
187        tokio::fs::metadata(self.patch_path(hash)).await.is_ok()
188    }
189
190    /// Check if an index exists in cache
191    pub async fn has_index(&self, hash: &str) -> bool {
192        tokio::fs::metadata(self.index_path(hash)).await.is_ok()
193    }
194
195    /// Write config data to cache
196    pub async fn write_config(&self, hash: &str, data: &[u8]) -> Result<()> {
197        let path = self.config_path(hash);
198
199        if let Some(parent) = path.parent() {
200            ensure_dir(parent).await?;
201        }
202
203        trace!("Writing {} bytes to config cache: {}", data.len(), hash);
204        tokio::fs::write(&path, data).await?;
205
206        Ok(())
207    }
208
209    /// Write data to cache
210    pub async fn write_data(&self, hash: &str, data: &[u8]) -> Result<()> {
211        let path = self.data_path(hash);
212
213        if let Some(parent) = path.parent() {
214            ensure_dir(parent).await?;
215        }
216
217        trace!("Writing {} bytes to data cache: {}", data.len(), hash);
218        tokio::fs::write(&path, data).await?;
219
220        Ok(())
221    }
222
223    /// Write patch data to cache
224    pub async fn write_patch(&self, hash: &str, data: &[u8]) -> Result<()> {
225        let path = self.patch_path(hash);
226
227        if let Some(parent) = path.parent() {
228            ensure_dir(parent).await?;
229        }
230
231        trace!("Writing {} bytes to patch cache: {}", data.len(), hash);
232        tokio::fs::write(&path, data).await?;
233
234        Ok(())
235    }
236
237    /// Write index to cache
238    pub async fn write_index(&self, hash: &str, data: &[u8]) -> Result<()> {
239        let path = self.index_path(hash);
240
241        if let Some(parent) = path.parent() {
242            ensure_dir(parent).await?;
243        }
244
245        trace!("Writing {} bytes to index cache: {}", data.len(), hash);
246        tokio::fs::write(&path, data).await?;
247
248        Ok(())
249    }
250
251    /// Read config from cache
252    pub async fn read_config(&self, hash: &str) -> Result<Vec<u8>> {
253        let path = self.config_path(hash);
254        trace!("Reading config from cache: {}", hash);
255        Ok(tokio::fs::read(&path).await?)
256    }
257
258    /// Read data from cache
259    pub async fn read_data(&self, hash: &str) -> Result<Vec<u8>> {
260        let path = self.data_path(hash);
261        trace!("Reading data from cache: {}", hash);
262        Ok(tokio::fs::read(&path).await?)
263    }
264
265    /// Stream data from cache to writer (memory-efficient)
266    pub async fn read_data_to_writer<W>(&self, hash: &str, mut writer: W) -> Result<u64>
267    where
268        W: tokio::io::AsyncWrite + Unpin,
269    {
270        let path = self.data_path(hash);
271        trace!("Streaming data from cache: {}", hash);
272
273        let mut file = tokio::fs::File::open(&path).await?;
274        let bytes_copied = tokio::io::copy(&mut file, &mut writer).await?;
275
276        Ok(bytes_copied)
277    }
278
279    /// Read patch from cache
280    pub async fn read_patch(&self, hash: &str) -> Result<Vec<u8>> {
281        let path = self.patch_path(hash);
282        trace!("Reading patch from cache: {}", hash);
283        Ok(tokio::fs::read(&path).await?)
284    }
285
286    /// Read index from cache
287    pub async fn read_index(&self, hash: &str) -> Result<Vec<u8>> {
288        let path = self.index_path(hash);
289        trace!("Reading index from cache: {}", hash);
290        Ok(tokio::fs::read(&path).await?)
291    }
292
293    /// Stream index from cache to writer (memory-efficient)
294    pub async fn read_index_to_writer<W>(&self, hash: &str, mut writer: W) -> Result<u64>
295    where
296        W: tokio::io::AsyncWrite + Unpin,
297    {
298        let path = self.index_path(hash);
299        trace!("Streaming index from cache: {}", hash);
300
301        let mut file = tokio::fs::File::open(&path).await?;
302        let bytes_copied = tokio::io::copy(&mut file, &mut writer).await?;
303
304        Ok(bytes_copied)
305    }
306
307    /// Stream read data from cache
308    ///
309    /// Returns a file handle for efficient streaming
310    pub async fn open_data(&self, hash: &str) -> Result<tokio::fs::File> {
311        let path = self.data_path(hash);
312        trace!("Opening data for streaming: {}", hash);
313        Ok(tokio::fs::File::open(&path).await?)
314    }
315
316    /// Get data size without reading it
317    pub async fn data_size(&self, hash: &str) -> Result<u64> {
318        let path = self.data_path(hash);
319        let metadata = tokio::fs::metadata(&path).await?;
320        Ok(metadata.len())
321    }
322
323    /// Get the base directory of this cache
324    pub fn base_dir(&self) -> &PathBuf {
325        &self.base_dir
326    }
327
328    /// Get the CDN path if set
329    pub fn cdn_path(&self) -> Option<&str> {
330        self.cdn_path.as_deref()
331    }
332
333    /// Write multiple config files in parallel
334    pub async fn write_configs_batch(&self, entries: &[(String, Vec<u8>)]) -> Result<()> {
335        use futures::future::try_join_all;
336
337        let futures = entries
338            .iter()
339            .map(|(hash, data)| self.write_config(hash, data));
340
341        try_join_all(futures).await?;
342        Ok(())
343    }
344
345    /// Write multiple data files in parallel
346    pub async fn write_data_batch(&self, entries: &[(String, Vec<u8>)]) -> Result<()> {
347        use futures::future::try_join_all;
348
349        let futures = entries
350            .iter()
351            .map(|(hash, data)| self.write_data(hash, data));
352
353        try_join_all(futures).await?;
354        Ok(())
355    }
356
357    /// Read multiple config files in parallel
358    pub async fn read_configs_batch(&self, hashes: &[String]) -> Vec<Result<Vec<u8>>> {
359        use futures::future::join_all;
360
361        let futures = hashes.iter().map(|hash| self.read_config(hash));
362        join_all(futures).await
363    }
364
365    /// Read multiple data files in parallel
366    pub async fn read_data_batch(&self, hashes: &[String]) -> Vec<Result<Vec<u8>>> {
367        use futures::future::join_all;
368
369        let futures = hashes.iter().map(|hash| self.read_data(hash));
370        join_all(futures).await
371    }
372
373    /// Check existence of multiple configs in parallel
374    pub async fn has_configs_batch(&self, hashes: &[String]) -> Vec<bool> {
375        use futures::future::join_all;
376
377        let futures = hashes.iter().map(|hash| self.has_config(hash));
378        join_all(futures).await
379    }
380
381    /// Check existence of multiple data files in parallel
382    pub async fn has_data_batch(&self, hashes: &[String]) -> Vec<bool> {
383        use futures::future::join_all;
384
385        let futures = hashes.iter().map(|hash| self.has_data(hash));
386        join_all(futures).await
387    }
388
389    /// Get sizes of multiple data files in parallel
390    pub async fn data_sizes_batch(&self, hashes: &[String]) -> Vec<Result<u64>> {
391        use futures::future::join_all;
392
393        let futures = hashes.iter().map(|hash| self.data_size(hash));
394        join_all(futures).await
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[tokio::test]
403    async fn test_cdn_cache_paths() {
404        let cache = CdnCache::new().await.unwrap();
405
406        let hash = "deadbeef1234567890abcdef12345678";
407
408        let config_path = cache.config_path(hash);
409        assert!(config_path.ends_with("config/de/ad/deadbeef1234567890abcdef12345678"));
410
411        let data_path = cache.data_path(hash);
412        assert!(data_path.ends_with("data/de/ad/deadbeef1234567890abcdef12345678"));
413
414        let patch_path = cache.patch_path(hash);
415        assert!(patch_path.ends_with("patch/de/ad/deadbeef1234567890abcdef12345678"));
416
417        let index_path = cache.index_path(hash);
418        assert!(index_path.ends_with("data/de/ad/deadbeef1234567890abcdef12345678.index"));
419    }
420
421    #[tokio::test]
422    async fn test_cdn_cache_with_cdn_path() {
423        let cache = CdnCache::with_cdn_path("tpr/wow").await.unwrap();
424
425        let hash = "deadbeef1234567890abcdef12345678";
426
427        let config_path = cache.config_path(hash);
428        assert!(config_path.ends_with("tpr/wow/config/de/ad/deadbeef1234567890abcdef12345678"));
429
430        let data_path = cache.data_path(hash);
431        assert!(data_path.ends_with("tpr/wow/data/de/ad/deadbeef1234567890abcdef12345678"));
432
433        let patch_path = cache.patch_path(hash);
434        assert!(patch_path.ends_with("tpr/wow/patch/de/ad/deadbeef1234567890abcdef12345678"));
435    }
436
437    #[tokio::test]
438    async fn test_cdn_product_cache() {
439        let cache = CdnCache::for_product("wow").await.unwrap();
440        assert!(cache.base_dir().ends_with("cdn/wow"));
441    }
442
443    #[tokio::test]
444    async fn test_cdn_cache_operations() {
445        let cache = CdnCache::for_product("test").await.unwrap();
446        let hash = "test5678901234567890abcdef123456";
447        let data = b"test data content";
448
449        // Write and read data
450        cache.write_data(hash, data).await.unwrap();
451        assert!(cache.has_data(hash).await);
452
453        let read_data = cache.read_data(hash).await.unwrap();
454        assert_eq!(read_data, data);
455
456        // Test size
457        let size = cache.data_size(hash).await.unwrap();
458        assert_eq!(size, data.len() as u64);
459
460        // Test config
461        let config_data = b"test config data";
462        cache.write_config(hash, config_data).await.unwrap();
463        assert!(cache.has_config(hash).await);
464
465        let read_config = cache.read_config(hash).await.unwrap();
466        assert_eq!(read_config, config_data);
467
468        // Cleanup
469        let _ = tokio::fs::remove_file(cache.data_path(hash)).await;
470        let _ = tokio::fs::remove_file(cache.config_path(hash)).await;
471    }
472}