1use std::path::PathBuf;
12use tracing::{debug, trace};
13
14use crate::{Result, ensure_dir, get_cache_dir};
15
16pub struct CdnCache {
18 base_dir: PathBuf,
20 cdn_path: Option<String>,
22}
23
24impl CdnCache {
25 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 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 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 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 pub fn set_cdn_path(&mut self, cdn_path: Option<String>) {
84 self.cdn_path = cdn_path;
85 }
86
87 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 pub fn config_dir(&self) -> PathBuf {
98 let base = self.effective_base_dir();
99 let path_str = base.to_string_lossy();
100
101 if path_str.ends_with("/config") || path_str.ends_with("\\config") {
103 base
105 } else if path_str.contains("configs/") || path_str.contains("configs\\") {
106 base
108 } else {
109 base.join("config")
111 }
112 }
113
114 pub fn data_dir(&self) -> PathBuf {
116 self.effective_base_dir().join("data")
117 }
118
119 pub fn patch_dir(&self) -> PathBuf {
121 self.effective_base_dir().join("patch")
122 }
123
124 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 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 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 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 pub async fn has_config(&self, hash: &str) -> bool {
177 tokio::fs::metadata(self.config_path(hash)).await.is_ok()
178 }
179
180 pub async fn has_data(&self, hash: &str) -> bool {
182 tokio::fs::metadata(self.data_path(hash)).await.is_ok()
183 }
184
185 pub async fn has_patch(&self, hash: &str) -> bool {
187 tokio::fs::metadata(self.patch_path(hash)).await.is_ok()
188 }
189
190 pub async fn has_index(&self, hash: &str) -> bool {
192 tokio::fs::metadata(self.index_path(hash)).await.is_ok()
193 }
194
195 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 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 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 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 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 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 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 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 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 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 pub fn base_dir(&self) -> &PathBuf {
297 &self.base_dir
298 }
299
300 pub fn cdn_path(&self) -> Option<&str> {
302 self.cdn_path.as_deref()
303 }
304
305 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 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 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 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 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 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 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 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 let size = cache.data_size(hash).await.unwrap();
436 assert_eq!(size, data.len() as u64);
437
438 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 let _ = tokio::fs::remove_file(cache.data_path(hash)).await;
448 let _ = tokio::fs::remove_file(cache.config_path(hash)).await;
449 }
450}