1use crate::error::{DatasetsError, Result};
4use scirs2_core::cache::{CacheBuilder, TTLSizedCache};
5use std::cell::RefCell;
6use std::fs::{self, File};
7use std::hash::{Hash, Hasher};
8use std::io::{Read, Write};
9use std::path::{Path, PathBuf};
10
11const CACHE_DIR_NAME: &str = "scirs2-datasets";
13
14const DEFAULT_CACHE_SIZE: usize = 100;
16
17const DEFAULT_CACHE_TTL: u64 = 3600; const DEFAULT_MAX_CACHE_SIZE: u64 = 500 * 1024 * 1024;
22
23const CACHE_DIR_ENV: &str = "SCIRS2_CACHE_DIR";
25
26#[allow(dead_code)]
28pub fn sha256_hash_file(path: &Path) -> std::result::Result<String, String> {
29 use sha2::{Digest, Sha256};
30
31 let mut file = File::open(path).map_err(|e| format!("Failed to open file: {e}"))?;
32 let mut hasher = Sha256::new();
33 let mut buffer = [0; 8192];
34
35 loop {
36 let bytes_read = file
37 .read(&mut buffer)
38 .map_err(|e| format!("Failed to read file: {e}"))?;
39 if bytes_read == 0 {
40 break;
41 }
42 hasher.update(&buffer[..bytes_read]);
43 }
44
45 Ok(hasher
46 .finalize()
47 .iter()
48 .map(|b| format!("{:02x}", b))
49 .collect())
50}
51
52pub struct RegistryEntry {
54 pub sha256: &'static str,
56 pub url: &'static str,
58}
59
60#[allow(dead_code)]
70pub fn get_cachedir() -> Result<PathBuf> {
71 if let Ok(cachedir) = std::env::var(CACHE_DIR_ENV) {
73 let cachepath = PathBuf::from(cachedir);
74 ensuredirectory_exists(&cachepath)?;
75 return Ok(cachepath);
76 }
77
78 if let Some(cachedir) = get_platform_cachedir() {
80 ensuredirectory_exists(&cachedir)?;
81 return Ok(cachedir);
82 }
83
84 let homedir = crate::platform_dirs::home_dir()
86 .ok_or_else(|| DatasetsError::CacheError("Could not find home directory".to_string()))?;
87 let cachedir = homedir.join(format!(".{CACHE_DIR_NAME}"));
88 ensuredirectory_exists(&cachedir)?;
89
90 Ok(cachedir)
91}
92
93#[allow(dead_code)]
95fn get_platform_cachedir() -> Option<PathBuf> {
96 #[cfg(target_os = "windows")]
97 {
98 crate::platform_dirs::data_local_dir().map(|dir| dir.join(CACHE_DIR_NAME))
99 }
100 #[cfg(target_os = "macos")]
101 {
102 crate::platform_dirs::home_dir()
103 .map(|dir| dir.join("Library").join("Caches").join(CACHE_DIR_NAME))
104 }
105 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
106 {
107 if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
109 Some(PathBuf::from(xdg_cache).join(CACHE_DIR_NAME))
110 } else {
111 crate::platform_dirs::home_dir().map(|home| home.join(".cache").join(CACHE_DIR_NAME))
112 }
113 }
114}
115
116#[allow(dead_code)]
118fn ensuredirectory_exists(dir: &Path) -> Result<()> {
119 if !dir.exists() {
120 fs::create_dir_all(dir).map_err(|e| {
121 DatasetsError::CacheError(format!("Failed to create cache directory: {e}"))
122 })?;
123 }
124 Ok(())
125}
126
127#[cfg(feature = "download-sync")]
145#[allow(dead_code)]
146pub fn fetch_data(
147 filename: &str,
148 registry_entry: Option<&RegistryEntry>,
149) -> std::result::Result<PathBuf, String> {
150 let cachedir = match get_cachedir() {
152 Ok(dir) => dir,
153 Err(e) => return Err(format!("Failed to get cache directory: {e}")),
154 };
155
156 let cachepath = cachedir.join(filename);
158 if cachepath.exists() {
159 return Ok(cachepath);
160 }
161
162 let entry = match registry_entry {
164 Some(entry) => entry,
165 None => return Err(format!("No registry entry found for {filename}")),
166 };
167
168 let tempdir = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
170 let temp_file = tempdir.path().join(filename);
171
172 crate::tls::ensure_default_tls_provider();
175 let response = ureq::get(entry.url)
176 .call()
177 .map_err(|e| format!("Failed to download {filename}: {e}"))?;
178
179 let mut body = response.into_body();
181 let bytes = body
182 .read_to_vec()
183 .map_err(|e| format!("Failed to read response body: {e}"))?;
184 let mut file = std::fs::File::create(&temp_file)
185 .map_err(|e| format!("Failed to create temp file: {e}"))?;
186 file.write_all(&bytes)
187 .map_err(|e| format!("Failed to write downloaded file: {e}"))?;
188
189 if !entry.sha256.is_empty() {
191 let computed_hash = sha256_hash_file(&temp_file)?;
192 if computed_hash != entry.sha256 {
193 return Err(format!(
194 "SHA256 hash mismatch for {filename}: expected {}, got {computed_hash}",
195 entry.sha256
196 ));
197 }
198 }
199
200 fs::create_dir_all(&cachedir).map_err(|e| format!("Failed to create cache dir: {e}"))?;
202 if let Some(parent) = cachepath.parent() {
203 fs::create_dir_all(parent).map_err(|e| format!("Failed to create cache dir: {e}"))?;
204 }
205
206 fs::copy(&temp_file, &cachepath).map_err(|e| format!("Failed to copy to cache: {e}"))?;
207
208 Ok(cachepath)
209}
210
211#[cfg(not(feature = "download-sync"))]
213#[allow(dead_code)]
214pub fn fetch_data(
215 _filename: &str,
216 _registry_entry: Option<&RegistryEntry>,
217) -> std::result::Result<PathBuf, String> {
218 Err("Synchronous download feature is disabled. Enable 'download-sync' feature.".to_string())
219}
220
221#[derive(Clone, Debug, Eq, PartialEq, Hash)]
223pub struct CacheKey {
224 name: String,
225 config_hash: String,
226}
227
228impl CacheKey {
229 pub fn new(name: &str, config: &crate::real_world::RealWorldConfig) -> Self {
231 use std::collections::hash_map::DefaultHasher;
232 use std::hash::{Hash, Hasher};
233
234 let mut hasher = DefaultHasher::new();
235 config.use_cache.hash(&mut hasher);
236 config.download_if_missing.hash(&mut hasher);
237 config.return_preprocessed.hash(&mut hasher);
238 config.subset.hash(&mut hasher);
239 config.random_state.hash(&mut hasher);
240
241 Self {
242 name: name.to_string(),
243 config_hash: format!("{:x}", hasher.finish()),
244 }
245 }
246
247 pub fn as_string(&self) -> String {
249 format!("{}_{}", self.name, self.config_hash)
250 }
251}
252
253#[derive(Clone, Debug, Eq, PartialEq)]
255struct FileCacheKey(String);
256
257impl Hash for FileCacheKey {
258 fn hash<H: Hasher>(&self, state: &mut H) {
259 self.0.hash(state);
260 }
261}
262
263pub struct DatasetCache {
268 cachedir: PathBuf,
270 mem_cache: RefCell<TTLSizedCache<FileCacheKey, Vec<u8>>>,
272 max_cache_size: u64,
274 offline_mode: bool,
276}
277
278impl Default for DatasetCache {
279 fn default() -> Self {
280 let cachedir = get_cachedir().expect("Could not get cache directory");
281
282 let mem_cache = RefCell::new(
283 CacheBuilder::new()
284 .with_size(DEFAULT_CACHE_SIZE)
285 .with_ttl(DEFAULT_CACHE_TTL)
286 .build_sized_cache(),
287 );
288
289 let offline_mode = std::env::var("SCIRS2_OFFLINE")
291 .map(|v| v.to_lowercase() == "true" || v == "1")
292 .unwrap_or(false);
293
294 DatasetCache {
295 cachedir,
296 mem_cache,
297 max_cache_size: DEFAULT_MAX_CACHE_SIZE,
298 offline_mode,
299 }
300 }
301}
302
303impl DatasetCache {
304 pub fn new(cachedir: PathBuf) -> Self {
306 let mem_cache = RefCell::new(
307 CacheBuilder::new()
308 .with_size(DEFAULT_CACHE_SIZE)
309 .with_ttl(DEFAULT_CACHE_TTL)
310 .build_sized_cache(),
311 );
312
313 let offline_mode = std::env::var("SCIRS2_OFFLINE")
314 .map(|v| v.to_lowercase() == "true" || v == "1")
315 .unwrap_or(false);
316
317 DatasetCache {
318 cachedir,
319 mem_cache,
320 max_cache_size: DEFAULT_MAX_CACHE_SIZE,
321 offline_mode,
322 }
323 }
324
325 pub fn with_config(cachedir: PathBuf, cache_size: usize, ttl_seconds: u64) -> Self {
327 let mem_cache = RefCell::new(
328 CacheBuilder::new()
329 .with_size(cache_size)
330 .with_ttl(ttl_seconds)
331 .build_sized_cache(),
332 );
333
334 let offline_mode = std::env::var("SCIRS2_OFFLINE")
335 .map(|v| v.to_lowercase() == "true" || v == "1")
336 .unwrap_or(false);
337
338 DatasetCache {
339 cachedir,
340 mem_cache,
341 max_cache_size: DEFAULT_MAX_CACHE_SIZE,
342 offline_mode,
343 }
344 }
345
346 pub fn with_full_config(
348 cachedir: PathBuf,
349 cache_size: usize,
350 ttl_seconds: u64,
351 max_cache_size: u64,
352 offline_mode: bool,
353 ) -> Self {
354 let mem_cache = RefCell::new(
355 CacheBuilder::new()
356 .with_size(cache_size)
357 .with_ttl(ttl_seconds)
358 .build_sized_cache(),
359 );
360
361 DatasetCache {
362 cachedir,
363 mem_cache,
364 max_cache_size,
365 offline_mode,
366 }
367 }
368
369 pub fn ensure_cachedir(&self) -> Result<()> {
371 if !self.cachedir.exists() {
372 fs::create_dir_all(&self.cachedir).map_err(|e| {
373 DatasetsError::CacheError(format!("Failed to create cache directory: {e}"))
374 })?;
375 }
376 Ok(())
377 }
378
379 pub fn get_cachedpath(&self, name: &str) -> PathBuf {
381 self.cachedir.join(name)
382 }
383
384 pub fn is_cached(&self, name: &str) -> bool {
386 let key = FileCacheKey(name.to_string());
388 if self.mem_cache.borrow_mut().get(&key).is_some() {
389 return true;
390 }
391
392 self.get_cachedpath(name).exists()
394 }
395
396 pub fn read_cached(&self, name: &str) -> Result<Vec<u8>> {
401 let key = FileCacheKey(name.to_string());
403 if let Some(data) = self.mem_cache.borrow_mut().get(&key) {
404 return Ok(data);
405 }
406
407 let path = self.get_cachedpath(name);
409 if !path.exists() {
410 return Err(DatasetsError::CacheError(format!(
411 "Cached file does not exist: {name}"
412 )));
413 }
414
415 let mut file = File::open(path)
416 .map_err(|e| DatasetsError::CacheError(format!("Failed to open cached file: {e}")))?;
417
418 let mut buffer = Vec::new();
419 file.read_to_end(&mut buffer)
420 .map_err(|e| DatasetsError::CacheError(format!("Failed to read cached file: {e}")))?;
421
422 self.mem_cache.borrow_mut().insert(key, buffer.clone());
424
425 Ok(buffer)
426 }
427
428 pub fn write_cached(&self, name: &str, data: &[u8]) -> Result<()> {
430 self.ensure_cachedir()?;
431
432 if self.max_cache_size > 0 {
434 let current_size = self.get_cache_size_bytes()?;
435 let new_file_size = data.len() as u64;
436
437 if current_size + new_file_size > self.max_cache_size {
438 self.cleanup_cache_to_fit(new_file_size)?;
439 }
440 }
441
442 let path = self.get_cachedpath(name);
444 let mut file = File::create(path)
445 .map_err(|e| DatasetsError::CacheError(format!("Failed to create cache file: {e}")))?;
446
447 file.write_all(data).map_err(|e| {
448 DatasetsError::CacheError(format!("Failed to write to cache file: {e}"))
449 })?;
450
451 let key = FileCacheKey(name.to_string());
453 self.mem_cache.borrow_mut().insert(key, data.to_vec());
454
455 Ok(())
456 }
457
458 pub fn clear_cache(&self) -> Result<()> {
460 if self.cachedir.exists() {
462 fs::remove_dir_all(&self.cachedir)
463 .map_err(|e| DatasetsError::CacheError(format!("Failed to clear cache: {e}")))?;
464 }
465
466 self.mem_cache.borrow_mut().clear();
468
469 Ok(())
470 }
471
472 pub fn remove_cached(&self, name: &str) -> Result<()> {
474 let path = self.get_cachedpath(name);
476 if path.exists() {
477 fs::remove_file(path).map_err(|e| {
478 DatasetsError::CacheError(format!("Failed to remove cached file: {e}"))
479 })?;
480 }
481
482 let key = FileCacheKey(name.to_string());
484 self.mem_cache.borrow_mut().remove(&key);
485
486 Ok(())
487 }
488
489 pub fn hash_filename(name: &str) -> String {
491 let hash = blake3::hash(name.as_bytes());
492 hash.to_hex().to_string()
493 }
494
495 pub fn get_cache_size_bytes(&self) -> Result<u64> {
497 let mut total_size = 0u64;
498
499 if self.cachedir.exists() {
500 let entries = fs::read_dir(&self.cachedir).map_err(|e| {
501 DatasetsError::CacheError(format!("Failed to read cache directory: {e}"))
502 })?;
503
504 for entry in entries {
505 let entry = entry.map_err(|e| {
506 DatasetsError::CacheError(format!("Failed to read directory entry: {e}"))
507 })?;
508
509 if let Ok(metadata) = entry.metadata() {
510 if metadata.is_file() {
511 total_size += metadata.len();
512 }
513 }
514 }
515 }
516
517 Ok(total_size)
518 }
519
520 fn cleanup_cache_to_fit(&self, needed_size: u64) -> Result<()> {
525 if self.max_cache_size == 0 {
526 return Ok(()); }
528
529 let current_size = self.get_cache_size_bytes()?;
530 let target_size = (self.max_cache_size as f64 * 0.8) as u64; let total_needed = current_size + needed_size;
532
533 if total_needed <= target_size {
534 return Ok(()); }
536
537 let size_to_free = total_needed - target_size;
538
539 let mut files_with_times = Vec::new();
541
542 if self.cachedir.exists() {
543 let entries = fs::read_dir(&self.cachedir).map_err(|e| {
544 DatasetsError::CacheError(format!("Failed to read cache directory: {e}"))
545 })?;
546
547 for entry in entries {
548 let entry = entry.map_err(|e| {
549 DatasetsError::CacheError(format!("Failed to read directory entry: {e}"))
550 })?;
551
552 if let Ok(metadata) = entry.metadata() {
553 if metadata.is_file() {
554 if let Ok(modified) = metadata.modified() {
555 files_with_times.push((entry.path(), metadata.len(), modified));
556 }
557 }
558 }
559 }
560 }
561
562 files_with_times.sort_by_key(|(_path, _size, modified)| *modified);
564
565 let mut freed_size = 0u64;
567 for (path, size, _modified) in files_with_times {
568 if freed_size >= size_to_free {
569 break;
570 }
571
572 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
574 let key = FileCacheKey(filename.to_string());
575 self.mem_cache.borrow_mut().remove(&key);
576 }
577
578 if let Err(e) = fs::remove_file(&path) {
580 eprintln!("Warning: Failed to remove cache file {path:?}: {e}");
581 } else {
582 freed_size += size;
583 }
584 }
585
586 Ok(())
587 }
588
589 pub fn set_offline_mode(&mut self, offline: bool) {
591 self.offline_mode = offline;
592 }
593
594 pub fn is_offline(&self) -> bool {
596 self.offline_mode
597 }
598
599 pub fn set_max_cache_size(&mut self, max_size: u64) {
601 self.max_cache_size = max_size;
602 }
603
604 pub fn max_cache_size(&self) -> u64 {
606 self.max_cache_size
607 }
608
609 pub fn put(&self, name: &str, data: &[u8]) -> Result<()> {
611 self.write_cached(name, data)
612 }
613
614 pub fn get_detailed_stats(&self) -> Result<DetailedCacheStats> {
616 let mut total_size = 0u64;
617 let mut file_count = 0usize;
618 let mut files = Vec::new();
619
620 if self.cachedir.exists() {
621 let entries = fs::read_dir(&self.cachedir).map_err(|e| {
622 DatasetsError::CacheError(format!("Failed to read cache directory: {e}"))
623 })?;
624
625 for entry in entries {
626 let entry = entry.map_err(|e| {
627 DatasetsError::CacheError(format!("Failed to read directory entry: {e}"))
628 })?;
629
630 if let Ok(metadata) = entry.metadata() {
631 if metadata.is_file() {
632 let size = metadata.len();
633 total_size += size;
634 file_count += 1;
635
636 if let Some(filename) = entry.file_name().to_str() {
637 files.push(CacheFileInfo {
638 name: filename.to_string(),
639 size_bytes: size,
640 modified: metadata.modified().ok(),
641 });
642 }
643 }
644 }
645 }
646 }
647
648 files.sort_by_key(|f| std::cmp::Reverse(f.size_bytes));
650
651 Ok(DetailedCacheStats {
652 total_size_bytes: total_size,
653 file_count,
654 cachedir: self.cachedir.clone(),
655 max_cache_size: self.max_cache_size,
656 offline_mode: self.offline_mode,
657 files,
658 })
659 }
660}
661
662#[cfg(feature = "download")]
664#[allow(dead_code)]
665pub fn download_data(_url: &str, force_download: bool) -> Result<Vec<u8>> {
666 let cache = DatasetCache::default();
667 let cache_key = DatasetCache::hash_filename(_url);
668
669 if !force_download && cache.is_cached(&cache_key) {
671 return cache.read_cached(&cache_key);
672 }
673
674 crate::tls::ensure_default_tls_provider();
677 let response = reqwest::blocking::get(_url).map_err(|e| {
678 DatasetsError::DownloadError(format!("Failed to download from {_url}: {e}"))
679 })?;
680
681 if !response.status().is_success() {
682 return Err(DatasetsError::DownloadError(format!(
683 "Failed to download from {_url}: HTTP status {}",
684 response.status()
685 )));
686 }
687
688 let data = response
689 .bytes()
690 .map_err(|e| DatasetsError::DownloadError(format!("Failed to read response data: {e}")))?;
691
692 let data_vec = data.to_vec();
693
694 cache.write_cached(&cache_key, &data_vec)?;
696
697 Ok(data_vec)
698}
699
700#[cfg(not(feature = "download"))]
702#[allow(dead_code)]
716pub fn download_data(_url: &str, _force_download: bool) -> Result<Vec<u8>> {
717 Err(DatasetsError::Other(
718 "Download feature is not enabled. Recompile with --features download".to_string(),
719 ))
720}
721
722pub struct CacheManager {
724 cache: DatasetCache,
725}
726
727impl CacheManager {
728 pub fn new() -> Result<Self> {
730 let cachedir = get_cachedir()?;
731 Ok(Self {
732 cache: DatasetCache::with_config(cachedir, DEFAULT_CACHE_SIZE, DEFAULT_CACHE_TTL),
733 })
734 }
735
736 pub fn with_config(cachedir: PathBuf, cache_size: usize, ttl_seconds: u64) -> Self {
738 Self {
739 cache: DatasetCache::with_config(cachedir, cache_size, ttl_seconds),
740 }
741 }
742
743 pub fn get(&self, key: &CacheKey) -> Result<Option<crate::utils::Dataset>> {
745 let name = key.as_string();
746 if self.cache.is_cached(&name) {
747 match self.cache.read_cached(&name) {
748 Ok(cached_data) => {
749 match serde_json::from_slice::<crate::utils::Dataset>(&cached_data) {
750 Ok(dataset) => Ok(Some(dataset)),
751 Err(e) => {
752 self.cache
754 .mem_cache
755 .borrow_mut()
756 .remove(&FileCacheKey(name.clone()));
757 Err(DatasetsError::CacheError(format!(
758 "Failed to deserialize cached dataset: {e}"
759 )))
760 }
761 }
762 }
763 Err(e) => Err(DatasetsError::CacheError(format!(
764 "Failed to read cached data: {e}"
765 ))),
766 }
767 } else {
768 Ok(None)
769 }
770 }
771
772 pub fn put(&self, key: &CacheKey, dataset: &crate::utils::Dataset) -> Result<()> {
774 let name = key.as_string();
775
776 let serialized = serde_json::to_vec(dataset)
778 .map_err(|e| DatasetsError::CacheError(format!("Failed to serialize dataset: {e}")))?;
779
780 self.cache
782 .write_cached(&name, &serialized)
783 .map_err(|e| DatasetsError::CacheError(format!("Failed to write to cache: {e}")))
784 }
785
786 pub fn with_full_config(
788 cachedir: PathBuf,
789 cache_size: usize,
790 ttl_seconds: u64,
791 max_cache_size: u64,
792 offline_mode: bool,
793 ) -> Self {
794 Self {
795 cache: DatasetCache::with_full_config(
796 cachedir,
797 cache_size,
798 ttl_seconds,
799 max_cache_size,
800 offline_mode,
801 ),
802 }
803 }
804
805 pub fn get_stats(&self) -> CacheStats {
807 let cachedir = &self.cache.cachedir;
808 let mut total_size = 0u64;
809 let mut file_count = 0usize;
810
811 if cachedir.exists() {
812 if let Ok(entries) = fs::read_dir(cachedir) {
813 for entry in entries.flatten() {
814 if let Ok(metadata) = entry.metadata() {
815 if metadata.is_file() {
816 total_size += metadata.len();
817 file_count += 1;
818 }
819 }
820 }
821 }
822 }
823
824 CacheStats {
825 total_size_bytes: total_size,
826 file_count,
827 cachedir: cachedir.clone(),
828 }
829 }
830
831 pub fn get_detailed_stats(&self) -> Result<DetailedCacheStats> {
833 self.cache.get_detailed_stats()
834 }
835
836 pub fn set_offline_mode(&mut self, offline: bool) {
838 self.cache.set_offline_mode(offline);
839 }
840
841 pub fn is_offline(&self) -> bool {
843 self.cache.is_offline()
844 }
845
846 pub fn set_max_cache_size(&mut self, max_size: u64) {
848 self.cache.set_max_cache_size(max_size);
849 }
850
851 pub fn max_cache_size(&self) -> u64 {
853 self.cache.max_cache_size()
854 }
855
856 pub fn clear_all(&self) -> Result<()> {
858 self.cache.clear_cache()
859 }
860
861 pub fn remove(&self, name: &str) -> Result<()> {
863 self.cache.remove_cached(name)
864 }
865
866 pub fn cleanup_old_files(&self, target_size: u64) -> Result<()> {
868 self.cache.cleanup_cache_to_fit(target_size)
869 }
870
871 pub fn list_cached_files(&self) -> Result<Vec<String>> {
873 let cachedir = &self.cache.cachedir;
874 let mut files = Vec::new();
875
876 if cachedir.exists() {
877 let entries = fs::read_dir(cachedir).map_err(|e| {
878 DatasetsError::CacheError(format!("Failed to read cache directory: {e}"))
879 })?;
880
881 for entry in entries {
882 let entry = entry.map_err(|e| {
883 DatasetsError::CacheError(format!("Failed to read directory entry: {e}"))
884 })?;
885
886 if let Some(filename) = entry.file_name().to_str() {
887 files.push(filename.to_string());
888 }
889 }
890 }
891
892 files.sort();
893 Ok(files)
894 }
895
896 pub fn cachedir(&self) -> &PathBuf {
898 &self.cache.cachedir
899 }
900
901 pub fn is_cached(&self, name: &str) -> bool {
903 self.cache.is_cached(name)
904 }
905
906 pub fn print_cache_report(&self) -> Result<()> {
908 let stats = self.get_detailed_stats()?;
909
910 println!("=== Cache Report ===");
911 println!("Cache Directory: {}", stats.cachedir.display());
912 println!(
913 "Total Size: {} ({} files)",
914 stats.formatted_size(),
915 stats.file_count
916 );
917 println!("Max Size: {}", stats.formatted_max_size());
918
919 if stats.max_cache_size > 0 {
920 println!("Usage: {:.1}%", stats.usage_percentage() * 100.0);
921 }
922
923 println!(
924 "Offline Mode: {}",
925 if stats.offline_mode {
926 "Enabled"
927 } else {
928 "Disabled"
929 }
930 );
931
932 if !stats.files.is_empty() {
933 println!("\nCached Files:");
934 for file in &stats.files {
935 println!(
936 " {} - {} ({})",
937 file.name,
938 file.formatted_size(),
939 file.formatted_modified()
940 );
941 }
942 }
943
944 Ok(())
945 }
946}
947
948pub struct CacheStats {
950 pub total_size_bytes: u64,
952 pub file_count: usize,
954 pub cachedir: PathBuf,
956}
957
958pub struct DetailedCacheStats {
960 pub total_size_bytes: u64,
962 pub file_count: usize,
964 pub cachedir: PathBuf,
966 pub max_cache_size: u64,
968 pub offline_mode: bool,
970 pub files: Vec<CacheFileInfo>,
972}
973
974#[derive(Debug, Clone)]
976pub struct CacheFileInfo {
977 pub name: String,
979 pub size_bytes: u64,
981 pub modified: Option<std::time::SystemTime>,
983}
984
985impl CacheStats {
986 pub fn formatted_size(&self) -> String {
988 format_bytes(self.total_size_bytes)
989 }
990}
991
992impl DetailedCacheStats {
993 pub fn formatted_size(&self) -> String {
995 format_bytes(self.total_size_bytes)
996 }
997
998 pub fn formatted_max_size(&self) -> String {
1000 if self.max_cache_size == 0 {
1001 "Unlimited".to_string()
1002 } else {
1003 format_bytes(self.max_cache_size)
1004 }
1005 }
1006
1007 pub fn usage_percentage(&self) -> f64 {
1009 if self.max_cache_size == 0 {
1010 0.0
1011 } else {
1012 self.total_size_bytes as f64 / self.max_cache_size as f64
1013 }
1014 }
1015}
1016
1017impl CacheFileInfo {
1018 pub fn formatted_size(&self) -> String {
1020 format_bytes(self.size_bytes)
1021 }
1022
1023 pub fn formatted_modified(&self) -> String {
1025 match &self.modified {
1026 Some(time) => {
1027 if let Ok(now) = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
1028 {
1029 if let Ok(modified) = time.duration_since(std::time::UNIX_EPOCH) {
1030 let diff_secs = now.as_secs().saturating_sub(modified.as_secs());
1031 let days = diff_secs / 86400;
1032 let hours = (diff_secs % 86400) / 3600;
1033 let mins = (diff_secs % 3600) / 60;
1034
1035 if days > 0 {
1036 format!("{days} days ago")
1037 } else if hours > 0 {
1038 format!("{hours} hours ago")
1039 } else if mins > 0 {
1040 format!("{mins} minutes ago")
1041 } else {
1042 "Just now".to_string()
1043 }
1044 } else {
1045 "Unknown".to_string()
1046 }
1047 } else {
1048 "Unknown".to_string()
1049 }
1050 }
1051 None => "Unknown".to_string(),
1052 }
1053 }
1054}
1055
1056#[allow(dead_code)]
1058fn format_bytes(bytes: u64) -> String {
1059 let size = bytes as f64;
1060 if size < 1024.0 {
1061 format!("{size} B")
1062 } else if size < 1024.0 * 1024.0 {
1063 format!("{:.1} KB", size / 1024.0)
1064 } else if size < 1024.0 * 1024.0 * 1024.0 {
1065 format!("{:.1} MB", size / (1024.0 * 1024.0))
1066 } else {
1067 format!("{:.1} GB", size / (1024.0 * 1024.0 * 1024.0))
1068 }
1069}
1070
1071#[derive(Debug, Clone)]
1073pub struct BatchResult {
1074 pub success_count: usize,
1076 pub failure_count: usize,
1078 pub failures: Vec<(String, String)>,
1080 pub total_bytes: u64,
1082 pub elapsed_time: std::time::Duration,
1084}
1085
1086impl BatchResult {
1087 pub fn new() -> Self {
1089 Self {
1090 success_count: 0,
1091 failure_count: 0,
1092 failures: Vec::new(),
1093 total_bytes: 0,
1094 elapsed_time: std::time::Duration::ZERO,
1095 }
1096 }
1097
1098 pub fn is_all_success(&self) -> bool {
1100 self.failure_count == 0
1101 }
1102
1103 pub fn success_rate(&self) -> f64 {
1105 let total = self.success_count + self.failure_count;
1106 if total == 0 {
1107 0.0
1108 } else {
1109 (self.success_count as f64 / total as f64) * 100.0
1110 }
1111 }
1112
1113 pub fn summary(&self) -> String {
1115 format!(
1116 "Batch completed: {}/{} successful ({:.1}%), {} bytes processed in {:.2}s",
1117 self.success_count,
1118 self.success_count + self.failure_count,
1119 self.success_rate(),
1120 format_bytes(self.total_bytes),
1121 self.elapsed_time.as_secs_f64()
1122 )
1123 }
1124}
1125
1126impl Default for BatchResult {
1127 fn default() -> Self {
1128 Self::new()
1129 }
1130}
1131
1132pub struct BatchOperations {
1134 cache: CacheManager,
1135 parallel: bool,
1136 max_retries: usize,
1137 retry_delay: std::time::Duration,
1138}
1139
1140impl BatchOperations {
1141 pub fn new(cache: CacheManager) -> Self {
1143 Self {
1144 cache,
1145 parallel: true,
1146 max_retries: 3,
1147 retry_delay: std::time::Duration::from_millis(1000),
1148 }
1149 }
1150
1151 pub fn with_parallel(mut self, parallel: bool) -> Self {
1153 self.parallel = parallel;
1154 self
1155 }
1156
1157 pub fn with_retry_config(
1159 mut self,
1160 max_retries: usize,
1161 retry_delay: std::time::Duration,
1162 ) -> Self {
1163 self.max_retries = max_retries;
1164 self.retry_delay = retry_delay;
1165 self
1166 }
1167
1168 #[cfg(feature = "download")]
1170 pub fn batch_download(&self, urls_andnames: &[(&str, &str)]) -> BatchResult {
1171 let start_time = std::time::Instant::now();
1172 let mut result = BatchResult::new();
1173
1174 if self.parallel {
1175 self.batch_download_parallel(urls_andnames, &mut result)
1176 } else {
1177 self.batch_download_sequential(urls_andnames, &mut result)
1178 }
1179
1180 result.elapsed_time = start_time.elapsed();
1181 result
1182 }
1183
1184 #[cfg(feature = "download")]
1185 fn batch_download_parallel(&self, urls_andnames: &[(&str, &str)], result: &mut BatchResult) {
1186 use std::fs::File;
1187 use std::io::Write;
1188 use std::sync::{Arc, Mutex};
1189 use std::thread;
1190
1191 if let Err(e) = self.cache.cache.ensure_cachedir() {
1193 result.failure_count += urls_andnames.len();
1194 for &(_, name) in urls_andnames {
1195 result
1196 .failures
1197 .push((name.to_string(), format!("Cache setup failed: {e}")));
1198 }
1199 return;
1200 }
1201
1202 let result_arc = Arc::new(Mutex::new(BatchResult::new()));
1203 let cachedir = self.cache.cache.cachedir.clone();
1204 let max_retries = self.max_retries;
1205 let retry_delay = self.retry_delay;
1206
1207 let handles: Vec<_> = urls_andnames
1208 .iter()
1209 .map(|&(url, name)| {
1210 let result_clone = Arc::clone(&result_arc);
1211 let url = url.to_string();
1212 let name = name.to_string();
1213 let cachedir = cachedir.clone();
1214
1215 thread::spawn(move || {
1216 let mut success = false;
1217 let mut last_error = String::new();
1218 let mut downloaded_data = Vec::new();
1219
1220 for attempt in 0..=max_retries {
1221 match download_data(&url, false) {
1222 Ok(data) => {
1223 let path = cachedir.join(&name);
1225 match File::create(&path) {
1226 Ok(mut file) => match file.write_all(&data) {
1227 Ok(_) => {
1228 let mut r =
1229 result_clone.lock().expect("Operation failed");
1230 r.success_count += 1;
1231 r.total_bytes += data.len() as u64;
1232 downloaded_data = data;
1233 success = true;
1234 break;
1235 }
1236 Err(e) => {
1237 last_error = format!("Failed to write cache file: {e}");
1238 }
1239 },
1240 Err(e) => {
1241 last_error = format!("Failed to create cache file: {e}");
1242 }
1243 }
1244 }
1245 Err(e) => {
1246 last_error = format!("Download failed: {e}");
1247 if attempt < max_retries {
1248 thread::sleep(retry_delay);
1249 }
1250 }
1251 }
1252 }
1253
1254 if !success {
1255 let mut r = result_clone.lock().expect("Operation failed");
1256 r.failure_count += 1;
1257 r.failures.push((name.clone(), last_error));
1258 }
1259
1260 (name, success, downloaded_data)
1261 })
1262 })
1263 .collect();
1264
1265 let mut successful_downloads = Vec::new();
1267 for handle in handles {
1268 if let Ok((name, success, data)) = handle.join() {
1269 if success && !data.is_empty() {
1270 successful_downloads.push((name, data));
1271 }
1272 }
1273 }
1274
1275 if let Ok(arc_result) = result_arc.lock() {
1277 result.success_count += arc_result.success_count;
1278 result.failure_count += arc_result.failure_count;
1279 result.failures.extend(arc_result.failures.clone());
1280 }
1281
1282 for (name, data) in successful_downloads {
1284 let key = FileCacheKey(name);
1285 self.cache.cache.mem_cache.borrow_mut().insert(key, data);
1286 }
1287 }
1288
1289 #[cfg(feature = "download")]
1290 fn batch_download_sequential(&self, urls_andnames: &[(&str, &str)], result: &mut BatchResult) {
1291 for &(url, name) in urls_andnames {
1292 let mut success = false;
1293 let mut last_error = String::new();
1294
1295 for attempt in 0..=self.max_retries {
1296 match download_data(url, false) {
1297 Ok(data) => match self.cache.cache.write_cached(name, &data) {
1298 Ok(_) => {
1299 result.success_count += 1;
1300 result.total_bytes += data.len() as u64;
1301 success = true;
1302 break;
1303 }
1304 Err(e) => {
1305 last_error = format!("Cache write failed: {e}");
1306 }
1307 },
1308 Err(e) => {
1309 last_error = format!("Download failed: {e}");
1310 if attempt < self.max_retries {
1311 std::thread::sleep(self.retry_delay);
1312 }
1313 }
1314 }
1315 }
1316
1317 if !success {
1318 result.failure_count += 1;
1319 result.failures.push((name.to_string(), last_error));
1320 }
1321 }
1322 }
1323
1324 pub fn batch_verify_integrity(&self, files_andhashes: &[(&str, &str)]) -> BatchResult {
1326 let start_time = std::time::Instant::now();
1327 let mut result = BatchResult::new();
1328
1329 for &(filename, expected_hash) in files_andhashes {
1330 match self.cache.cache.get_cachedpath(filename).exists() {
1331 true => match sha256_hash_file(&self.cache.cache.get_cachedpath(filename)) {
1332 Ok(actual_hash) => {
1333 if actual_hash == expected_hash {
1334 result.success_count += 1;
1335 if let Ok(metadata) =
1336 std::fs::metadata(self.cache.cache.get_cachedpath(filename))
1337 {
1338 result.total_bytes += metadata.len();
1339 }
1340 } else {
1341 result.failure_count += 1;
1342 result.failures.push((
1343 filename.to_string(),
1344 format!(
1345 "Hash mismatch: expected {expected_hash}, got {actual_hash}"
1346 ),
1347 ));
1348 }
1349 }
1350 Err(e) => {
1351 result.failure_count += 1;
1352 result.failures.push((
1353 filename.to_string(),
1354 format!("Hash computation failed: {e}"),
1355 ));
1356 }
1357 },
1358 false => {
1359 result.failure_count += 1;
1360 result
1361 .failures
1362 .push((filename.to_string(), "File not found in cache".to_string()));
1363 }
1364 }
1365 }
1366
1367 result.elapsed_time = start_time.elapsed();
1368 result
1369 }
1370
1371 pub fn selective_cleanup(
1373 &self,
1374 patterns: &[&str],
1375 max_age_days: Option<u32>,
1376 ) -> Result<BatchResult> {
1377 let start_time = std::time::Instant::now();
1378 let mut result = BatchResult::new();
1379
1380 let cached_files = self.cache.list_cached_files()?;
1381 let now = std::time::SystemTime::now();
1382
1383 for filename in cached_files {
1384 let should_remove = patterns.iter().any(|pattern| {
1385 filename.contains(pattern) || matches_glob_pattern(&filename, pattern)
1386 });
1387
1388 if should_remove {
1389 let filepath = self.cache.cache.get_cachedpath(&filename);
1390
1391 let remove_due_to_age = if let Some(max_age) = max_age_days {
1393 if let Ok(metadata) = std::fs::metadata(&filepath) {
1394 if let Ok(modified) = metadata.modified() {
1395 if let Ok(age) = now.duration_since(modified) {
1396 age.as_secs() > (max_age as u64 * 24 * 3600)
1397 } else {
1398 false
1399 }
1400 } else {
1401 false
1402 }
1403 } else {
1404 false
1405 }
1406 } else {
1407 true };
1409
1410 if remove_due_to_age {
1411 match self.cache.remove(&filename) {
1412 Ok(_) => {
1413 result.success_count += 1;
1414 if let Ok(metadata) = std::fs::metadata(&filepath) {
1415 result.total_bytes += metadata.len();
1416 }
1417 }
1418 Err(e) => {
1419 result.failure_count += 1;
1420 result
1421 .failures
1422 .push((filename, format!("Removal failed: {e}")));
1423 }
1424 }
1425 }
1426 }
1427 }
1428
1429 result.elapsed_time = start_time.elapsed();
1430 Ok(result)
1431 }
1432
1433 pub fn batch_process<F, T, E>(&self, names: &[String], processor: F) -> BatchResult
1435 where
1436 F: Fn(&str, &[u8]) -> std::result::Result<T, E> + Sync + Send + 'static,
1437 E: std::fmt::Display,
1438 T: Send,
1439 {
1440 let start_time = std::time::Instant::now();
1441 let mut result = BatchResult::new();
1442
1443 if self.parallel {
1444 self.batch_process_parallel(names, processor, &mut result)
1445 } else {
1446 self.batch_process_sequential(names, processor, &mut result)
1447 }
1448
1449 result.elapsed_time = start_time.elapsed();
1450 result
1451 }
1452
1453 fn batch_process_parallel<F, T, E>(
1454 &self,
1455 names: &[String],
1456 processor: F,
1457 result: &mut BatchResult,
1458 ) where
1459 F: Fn(&str, &[u8]) -> std::result::Result<T, E> + Sync + Send + 'static,
1460 E: std::fmt::Display,
1461 T: Send,
1462 {
1463 let mut data_pairs = Vec::new();
1466
1467 for name in names {
1469 match self.cache.cache.read_cached(name) {
1470 Ok(data) => data_pairs.push((name.clone(), data)),
1471 Err(e) => {
1472 result.failure_count += 1;
1473 result
1474 .failures
1475 .push((name.clone(), format!("Cache read failed: {e}")));
1476 }
1477 }
1478 }
1479
1480 if !data_pairs.is_empty() {
1482 use std::sync::{Arc, Mutex};
1483 use std::thread;
1484
1485 let parallel_result = Arc::new(Mutex::new(BatchResult::new()));
1486 let processor = Arc::new(processor);
1487
1488 let handles: Vec<_> = data_pairs
1489 .into_iter()
1490 .map(|(name, data)| {
1491 let result_clone = Arc::clone(¶llel_result);
1492 let processor_clone = Arc::clone(&processor);
1493
1494 thread::spawn(move || match processor_clone(&name, &data) {
1495 Ok(_) => {
1496 let mut r = result_clone.lock().expect("Operation failed");
1497 r.success_count += 1;
1498 r.total_bytes += data.len() as u64;
1499 }
1500 Err(e) => {
1501 let mut r = result_clone.lock().expect("Operation failed");
1502 r.failure_count += 1;
1503 r.failures.push((name, format!("Processing failed: {e}")));
1504 }
1505 })
1506 })
1507 .collect();
1508
1509 for handle in handles {
1510 let _ = handle.join();
1511 }
1512
1513 let parallel_result = parallel_result.lock().expect("Operation failed");
1515 result.success_count += parallel_result.success_count;
1516 result.failure_count += parallel_result.failure_count;
1517 result.total_bytes += parallel_result.total_bytes;
1518 result.failures.extend(parallel_result.failures.clone());
1519 }
1520 }
1521
1522 fn batch_process_sequential<F, T, E>(
1523 &self,
1524 names: &[String],
1525 processor: F,
1526 result: &mut BatchResult,
1527 ) where
1528 F: Fn(&str, &[u8]) -> std::result::Result<T, E>,
1529 E: std::fmt::Display,
1530 {
1531 for name in names {
1532 match self.cache.cache.read_cached(name) {
1533 Ok(data) => match processor(name, &data) {
1534 Ok(_) => {
1535 result.success_count += 1;
1536 result.total_bytes += data.len() as u64;
1537 }
1538 Err(e) => {
1539 result.failure_count += 1;
1540 result
1541 .failures
1542 .push((name.clone(), format!("Processing failed: {e}")));
1543 }
1544 },
1545 Err(e) => {
1546 result.failure_count += 1;
1547 result
1548 .failures
1549 .push((name.clone(), format!("Cache read failed: {e}")));
1550 }
1551 }
1552 }
1553 }
1554
1555 pub fn cache_manager(&self) -> &CacheManager {
1557 &self.cache
1558 }
1559
1560 pub fn write_cached(&self, name: &str, data: &[u8]) -> Result<()> {
1562 self.cache.cache.write_cached(name, data)
1563 }
1564
1565 pub fn read_cached(&self, name: &str) -> Result<Vec<u8>> {
1567 self.cache.cache.read_cached(name)
1568 }
1569
1570 pub fn list_cached_files(&self) -> Result<Vec<String>> {
1572 self.cache.list_cached_files()
1573 }
1574
1575 pub fn print_cache_report(&self) -> Result<()> {
1577 self.cache.print_cache_report()
1578 }
1579
1580 pub fn get_cache_statistics(&self) -> Result<BatchResult> {
1582 let start_time = std::time::Instant::now();
1583 let mut result = BatchResult::new();
1584
1585 let cached_files = self.cache.list_cached_files()?;
1586
1587 for filename in cached_files {
1588 let filepath = self.cache.cache.get_cachedpath(&filename);
1589 match std::fs::metadata(&filepath) {
1590 Ok(metadata) => {
1591 result.success_count += 1;
1592 result.total_bytes += metadata.len();
1593 }
1594 Err(e) => {
1595 result.failure_count += 1;
1596 result
1597 .failures
1598 .push((filename, format!("Metadata read failed: {e}")));
1599 }
1600 }
1601 }
1602
1603 result.elapsed_time = start_time.elapsed();
1604 Ok(result)
1605 }
1606}
1607
1608#[allow(dead_code)]
1610fn matches_glob_pattern(filename: &str, pattern: &str) -> bool {
1611 if pattern == "*" {
1612 return true;
1613 }
1614
1615 if pattern.contains('*') {
1616 let parts: Vec<&str> = pattern.split('*').collect();
1617 if parts.len() == 2 {
1618 let prefix = parts[0];
1619 let suffix = parts[1];
1620 return filename.starts_with(prefix) && filename.ends_with(suffix);
1621 }
1622 }
1623
1624 filename == pattern
1625}
1626
1627#[cfg(test)]
1628mod tests {
1629 use super::*;
1630 use tempfile::TempDir;
1631
1632 #[test]
1633 fn test_batch_result() {
1634 let mut result = BatchResult::new();
1635 assert_eq!(result.success_count, 0);
1636 assert_eq!(result.failure_count, 0);
1637 assert!(result.is_all_success());
1638 assert_eq!(result.success_rate(), 0.0);
1639
1640 result.success_count = 8;
1641 result.failure_count = 2;
1642 result.total_bytes = 1024;
1643
1644 assert!(!result.is_all_success());
1645 assert_eq!(result.success_rate(), 80.0);
1646 assert!(result.summary().contains("8/10 successful"));
1647 assert!(result.summary().contains("80.0%"));
1648 }
1649
1650 #[test]
1651 fn test_batch_operations_creation() {
1652 let tempdir = TempDir::new().expect("Operation failed");
1653 let cache_manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1654 let batch_ops = BatchOperations::new(cache_manager)
1655 .with_parallel(false)
1656 .with_retry_config(2, std::time::Duration::from_millis(500));
1657
1658 assert!(!batch_ops.parallel);
1659 assert_eq!(batch_ops.max_retries, 2);
1660 }
1661
1662 #[test]
1663 fn test_selective_cleanup() {
1664 let tempdir = TempDir::new().expect("Operation failed");
1665 let cache_manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1666 let batch_ops = BatchOperations::new(cache_manager);
1667
1668 let test_data = vec![0u8; 100];
1670 batch_ops
1671 .cache
1672 .cache
1673 .write_cached("test1.csv", &test_data)
1674 .expect("Test: cache operation failed");
1675 batch_ops
1676 .cache
1677 .cache
1678 .write_cached("test2.csv", &test_data)
1679 .expect("Test: cache operation failed");
1680 batch_ops
1681 .cache
1682 .cache
1683 .write_cached("data.json", &test_data)
1684 .expect("Test: cache operation failed");
1685
1686 let result = batch_ops
1688 .selective_cleanup(&["*.csv"], None)
1689 .expect("Operation failed");
1690
1691 assert_eq!(result.success_count, 2); assert!(!batch_ops.cache.is_cached("test1.csv"));
1693 assert!(!batch_ops.cache.is_cached("test2.csv"));
1694 assert!(batch_ops.cache.is_cached("data.json")); }
1696
1697 #[test]
1698 fn test_batch_process() {
1699 let tempdir = TempDir::new().expect("Operation failed");
1700 let cache_manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1701 let batch_ops = BatchOperations::new(cache_manager).with_parallel(false);
1702
1703 let test_data1 = vec![1u8; 100];
1705 let test_data2 = vec![2u8; 200];
1706 batch_ops
1707 .cache
1708 .cache
1709 .write_cached("file1.dat", &test_data1)
1710 .expect("Test: cache operation failed");
1711 batch_ops
1712 .cache
1713 .cache
1714 .write_cached("file2.dat", &test_data2)
1715 .expect("Test: cache operation failed");
1716
1717 let files = vec!["file1.dat".to_string(), "file2.dat".to_string()];
1718
1719 let result = batch_ops.batch_process(&files, |_name, data| {
1721 if data.is_empty() {
1722 Err("Empty file")
1723 } else {
1724 Ok(data.len())
1725 }
1726 });
1727
1728 assert_eq!(result.success_count, 2);
1729 assert_eq!(result.failure_count, 0);
1730 assert_eq!(result.total_bytes, 300); }
1732
1733 #[test]
1734 fn test_get_cache_statistics() {
1735 let tempdir = TempDir::new().expect("Operation failed");
1736 let cache_manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1737 let batch_ops = BatchOperations::new(cache_manager);
1738
1739 let result = batch_ops.get_cache_statistics().expect("Operation failed");
1741 assert_eq!(result.success_count, 0);
1742
1743 let test_data = vec![0u8; 500];
1745 batch_ops
1746 .cache
1747 .cache
1748 .write_cached("test1.dat", &test_data)
1749 .expect("Test: cache operation failed");
1750 batch_ops
1751 .cache
1752 .cache
1753 .write_cached("test2.dat", &test_data)
1754 .expect("Test: cache operation failed");
1755
1756 let result = batch_ops.get_cache_statistics().expect("Operation failed");
1757 assert_eq!(result.success_count, 2);
1758 assert_eq!(result.total_bytes, 1000);
1759 }
1760
1761 #[test]
1762 fn test_matches_glob_pattern() {
1763 assert!(matches_glob_pattern("test.csv", "*"));
1764 assert!(matches_glob_pattern("test.csv", "*.csv"));
1765 assert!(matches_glob_pattern("test.csv", "test.*"));
1766 assert!(matches_glob_pattern("test.csv", "test.csv"));
1767
1768 assert!(!matches_glob_pattern("test.json", "*.csv"));
1769 assert!(!matches_glob_pattern("other.csv", "test.*"));
1770 }
1771
1772 #[test]
1773 fn test_cache_manager_creation() {
1774 let tempdir = TempDir::new().expect("Operation failed");
1775 let manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1776 let stats = manager.get_stats();
1777 assert_eq!(stats.file_count, 0);
1778 }
1779
1780 #[test]
1781 fn test_cache_stats_formatting() {
1782 let tempdir = TempDir::new().expect("Operation failed");
1783 let stats = CacheStats {
1784 total_size_bytes: 1024,
1785 file_count: 1,
1786 cachedir: tempdir.path().to_path_buf(),
1787 };
1788
1789 assert_eq!(stats.formatted_size(), "1.0 KB");
1790
1791 let stats_large = CacheStats {
1792 total_size_bytes: 1024 * 1024 * 1024,
1793 file_count: 1,
1794 cachedir: tempdir.path().to_path_buf(),
1795 };
1796
1797 assert_eq!(stats_large.formatted_size(), "1.0 GB");
1798 }
1799
1800 #[test]
1801 fn test_hash_file_name() {
1802 let hash1 = DatasetCache::hash_filename("test.csv");
1803 let hash2 = DatasetCache::hash_filename("test.csv");
1804 let hash3 = DatasetCache::hash_filename("different.csv");
1805
1806 assert_eq!(hash1, hash2);
1807 assert_ne!(hash1, hash3);
1808 assert_eq!(hash1.len(), 64); }
1810
1811 #[test]
1812 fn test_platform_cachedir() {
1813 let cachedir = get_platform_cachedir();
1814 assert!(cachedir.is_some() || cfg!(target_os = "unknown"));
1816
1817 if let Some(dir) = cachedir {
1818 assert!(dir.to_string_lossy().contains("scirs2-datasets"));
1819 }
1820 }
1821
1822 #[test]
1823 fn test_cache_size_management() {
1824 let tempdir = TempDir::new().expect("Operation failed");
1825 let cache = DatasetCache::with_full_config(
1826 tempdir.path().to_path_buf(),
1827 10,
1828 3600,
1829 2048, false,
1831 );
1832
1833 let small_data1 = vec![0u8; 400];
1835 cache
1836 .write_cached("small1.dat", &small_data1)
1837 .expect("Operation failed");
1838
1839 let small_data2 = vec![0u8; 400];
1840 cache
1841 .write_cached("small2.dat", &small_data2)
1842 .expect("Operation failed");
1843
1844 let small_data3 = vec![0u8; 400];
1845 cache
1846 .write_cached("small3.dat", &small_data3)
1847 .expect("Operation failed");
1848
1849 let medium_data = vec![0u8; 800];
1851 cache
1852 .write_cached("medium.dat", &medium_data)
1853 .expect("Operation failed");
1854
1855 let stats = cache.get_detailed_stats().expect("Operation failed");
1857 assert!(stats.total_size_bytes <= cache.max_cache_size());
1858
1859 assert!(cache.is_cached("medium.dat"));
1861 }
1862
1863 #[test]
1864 fn test_offline_mode() {
1865 let tempdir = TempDir::new().expect("Operation failed");
1866 let mut cache = DatasetCache::new(tempdir.path().to_path_buf());
1867
1868 assert!(!cache.is_offline());
1869 cache.set_offline_mode(true);
1870 assert!(cache.is_offline());
1871 }
1872
1873 #[test]
1874 fn test_detailed_stats() {
1875 let tempdir = TempDir::new().expect("Operation failed");
1876 let cache = DatasetCache::new(tempdir.path().to_path_buf());
1877
1878 let test_data = vec![1, 2, 3, 4, 5];
1879 cache
1880 .write_cached("test.dat", &test_data)
1881 .expect("Operation failed");
1882
1883 let stats = cache.get_detailed_stats().expect("Operation failed");
1884 assert_eq!(stats.file_count, 1);
1885 assert_eq!(stats.total_size_bytes, test_data.len() as u64);
1886 assert_eq!(stats.files.len(), 1);
1887 assert_eq!(stats.files[0].name, "test.dat");
1888 assert_eq!(stats.files[0].size_bytes, test_data.len() as u64);
1889 }
1890
1891 #[test]
1892 fn test_cache_manager() {
1893 let tempdir = TempDir::new().expect("Operation failed");
1894 let manager = CacheManager::with_config(tempdir.path().to_path_buf(), 10, 3600);
1895
1896 let stats = manager.get_stats();
1897 assert_eq!(stats.file_count, 0);
1898 assert_eq!(stats.total_size_bytes, 0);
1899
1900 assert_eq!(manager.cachedir(), &tempdir.path().to_path_buf());
1901 }
1902
1903 #[test]
1904 fn test_format_bytes() {
1905 assert_eq!(format_bytes(512), "512 B");
1906 assert_eq!(format_bytes(1024), "1.0 KB");
1907 assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
1908 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
1909 }
1910}