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    /// Read patch from cache
266    pub async fn read_patch(&self, hash: &str) -> Result<Vec<u8>> {
267        let path = self.patch_path(hash);
268        trace!("Reading patch from cache: {}", hash);
269        Ok(tokio::fs::read(&path).await?)
270    }
271
272    /// Read index from cache
273    pub async fn read_index(&self, hash: &str) -> Result<Vec<u8>> {
274        let path = self.index_path(hash);
275        trace!("Reading index from cache: {}", hash);
276        Ok(tokio::fs::read(&path).await?)
277    }
278
279    /// Stream read data from cache
280    ///
281    /// Returns a file handle for efficient streaming
282    pub async fn open_data(&self, hash: &str) -> Result<tokio::fs::File> {
283        let path = self.data_path(hash);
284        trace!("Opening data for streaming: {}", hash);
285        Ok(tokio::fs::File::open(&path).await?)
286    }
287
288    /// Get data size without reading it
289    pub async fn data_size(&self, hash: &str) -> Result<u64> {
290        let path = self.data_path(hash);
291        let metadata = tokio::fs::metadata(&path).await?;
292        Ok(metadata.len())
293    }
294
295    /// Get the base directory of this cache
296    pub fn base_dir(&self) -> &PathBuf {
297        &self.base_dir
298    }
299
300    /// Get the CDN path if set
301    pub fn cdn_path(&self) -> Option<&str> {
302        self.cdn_path.as_deref()
303    }
304
305    /// Write multiple config files in parallel
306    pub async fn write_configs_batch(&self, entries: &[(String, Vec<u8>)]) -> Result<()> {
307        use futures::future::try_join_all;
308
309        let futures = entries
310            .iter()
311            .map(|(hash, data)| self.write_config(hash, data));
312
313        try_join_all(futures).await?;
314        Ok(())
315    }
316
317    /// Write multiple data files in parallel
318    pub async fn write_data_batch(&self, entries: &[(String, Vec<u8>)]) -> Result<()> {
319        use futures::future::try_join_all;
320
321        let futures = entries
322            .iter()
323            .map(|(hash, data)| self.write_data(hash, data));
324
325        try_join_all(futures).await?;
326        Ok(())
327    }
328
329    /// Read multiple config files in parallel
330    pub async fn read_configs_batch(&self, hashes: &[String]) -> Vec<Result<Vec<u8>>> {
331        use futures::future::join_all;
332
333        let futures = hashes.iter().map(|hash| self.read_config(hash));
334        join_all(futures).await
335    }
336
337    /// Read multiple data files in parallel
338    pub async fn read_data_batch(&self, hashes: &[String]) -> Vec<Result<Vec<u8>>> {
339        use futures::future::join_all;
340
341        let futures = hashes.iter().map(|hash| self.read_data(hash));
342        join_all(futures).await
343    }
344
345    /// Check existence of multiple configs in parallel
346    pub async fn has_configs_batch(&self, hashes: &[String]) -> Vec<bool> {
347        use futures::future::join_all;
348
349        let futures = hashes.iter().map(|hash| self.has_config(hash));
350        join_all(futures).await
351    }
352
353    /// Check existence of multiple data files in parallel
354    pub async fn has_data_batch(&self, hashes: &[String]) -> Vec<bool> {
355        use futures::future::join_all;
356
357        let futures = hashes.iter().map(|hash| self.has_data(hash));
358        join_all(futures).await
359    }
360
361    /// Get sizes of multiple data files in parallel
362    pub async fn data_sizes_batch(&self, hashes: &[String]) -> Vec<Result<u64>> {
363        use futures::future::join_all;
364
365        let futures = hashes.iter().map(|hash| self.data_size(hash));
366        join_all(futures).await
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[tokio::test]
375    async fn test_cdn_cache_paths() {
376        let cache = CdnCache::new().await.unwrap();
377
378        let hash = "deadbeef1234567890abcdef12345678";
379
380        let config_path = cache.config_path(hash);
381        assert!(config_path.to_str().unwrap().contains("config/de/ad"));
382
383        let data_path = cache.data_path(hash);
384        assert!(data_path.to_str().unwrap().contains("data/de/ad"));
385
386        let patch_path = cache.patch_path(hash);
387        assert!(patch_path.to_str().unwrap().contains("patch/de/ad"));
388
389        let index_path = cache.index_path(hash);
390        assert!(index_path.to_str().unwrap().contains("data/de/ad"));
391        assert!(index_path.to_str().unwrap().ends_with(".index"));
392    }
393
394    #[tokio::test]
395    async fn test_cdn_cache_with_cdn_path() {
396        let cache = CdnCache::with_cdn_path("tpr/wow").await.unwrap();
397
398        let hash = "deadbeef1234567890abcdef12345678";
399
400        let config_path = cache.config_path(hash);
401        assert!(
402            config_path
403                .to_str()
404                .unwrap()
405                .contains("tpr/wow/config/de/ad")
406        );
407
408        let data_path = cache.data_path(hash);
409        assert!(data_path.to_str().unwrap().contains("tpr/wow/data/de/ad"));
410
411        let patch_path = cache.patch_path(hash);
412        assert!(patch_path.to_str().unwrap().contains("tpr/wow/patch/de/ad"));
413    }
414
415    #[tokio::test]
416    async fn test_cdn_product_cache() {
417        let cache = CdnCache::for_product("wow").await.unwrap();
418        assert!(cache.base_dir().to_str().unwrap().contains("cdn/wow"));
419    }
420
421    #[tokio::test]
422    async fn test_cdn_cache_operations() {
423        let cache = CdnCache::for_product("test").await.unwrap();
424        let hash = "test5678901234567890abcdef123456";
425        let data = b"test data content";
426
427        // Write and read data
428        cache.write_data(hash, data).await.unwrap();
429        assert!(cache.has_data(hash).await);
430
431        let read_data = cache.read_data(hash).await.unwrap();
432        assert_eq!(read_data, data);
433
434        // Test size
435        let size = cache.data_size(hash).await.unwrap();
436        assert_eq!(size, data.len() as u64);
437
438        // Test config
439        let config_data = b"test config data";
440        cache.write_config(hash, config_data).await.unwrap();
441        assert!(cache.has_config(hash).await);
442
443        let read_config = cache.read_config(hash).await.unwrap();
444        assert_eq!(read_config, config_data);
445
446        // Cleanup
447        let _ = tokio::fs::remove_file(cache.data_path(hash)).await;
448        let _ = tokio::fs::remove_file(cache.config_path(hash)).await;
449    }
450}