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_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 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 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 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 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 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 pub fn base_dir(&self) -> &PathBuf {
325 &self.base_dir
326 }
327
328 pub fn cdn_path(&self) -> Option<&str> {
330 self.cdn_path.as_deref()
331 }
332
333 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 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 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 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 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 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 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 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 let size = cache.data_size(hash).await.unwrap();
458 assert_eq!(size, data.len() as u64);
459
460 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 let _ = tokio::fs::remove_file(cache.data_path(hash)).await;
470 let _ = tokio::fs::remove_file(cache.config_path(hash)).await;
471 }
472}