//! Voice message cache management. //! //! Caches downloaded OGG voice files in ~/.cache/tele-tui/voice/ //! with LRU eviction when cache size exceeds limit. use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; /// Cache for voice message files pub struct VoiceCache { cache_dir: PathBuf, /// file_id -> (path, size_bytes, access_count) files: HashMap, access_counter: usize, max_size_bytes: u64, } impl VoiceCache { /// Creates a new VoiceCache with the given max size in MB pub fn new(max_size_mb: u64) -> Result { let cache_dir = dirs::cache_dir() .ok_or("Failed to get cache directory")? .join("tele-tui") .join("voice"); fs::create_dir_all(&cache_dir) .map_err(|e| format!("Failed to create cache directory: {}", e))?; Ok(Self { cache_dir, files: HashMap::new(), access_counter: 0, max_size_bytes: max_size_mb * 1024 * 1024, }) } /// Gets the path for a cached voice file, if it exists pub fn get(&mut self, file_id: &str) -> Option { if let Some((path, _, access)) = self.files.get_mut(file_id) { // Update access count for LRU self.access_counter += 1; *access = self.access_counter; Some(path.clone()) } else { None } } /// Stores a voice file in the cache pub fn store(&mut self, file_id: &str, source_path: &Path) -> Result { // Copy file to cache let filename = format!("{}.ogg", file_id.replace('/', "_")); let dest_path = self.cache_dir.join(&filename); fs::copy(source_path, &dest_path) .map_err(|e| format!("Failed to copy voice file to cache: {}", e))?; // Get file size let size = fs::metadata(&dest_path) .map_err(|e| format!("Failed to get file size: {}", e))? .len(); // Store in cache self.access_counter += 1; self.files .insert(file_id.to_string(), (dest_path.clone(), size, self.access_counter)); // Check if we need to evict self.evict_if_needed()?; Ok(dest_path) } /// Returns the total size of all cached files pub fn total_size(&self) -> u64 { self.files.values().map(|(_, size, _)| size).sum() } /// Evicts oldest files until cache is under max size fn evict_if_needed(&mut self) -> Result<(), String> { while self.total_size() > self.max_size_bytes && !self.files.is_empty() { // Find least recently accessed file let oldest_id = self .files .iter() .min_by_key(|(_, (_, _, access))| access) .map(|(id, _)| id.clone()); if let Some(id) = oldest_id { self.evict(&id)?; } } Ok(()) } /// Evicts a specific file from cache fn evict(&mut self, file_id: &str) -> Result<(), String> { if let Some((path, _, _)) = self.files.remove(file_id) { fs::remove_file(&path).map_err(|e| format!("Failed to remove cached file: {}", e))?; } Ok(()) } /// Clears all cached files #[allow(dead_code)] pub fn clear(&mut self) -> Result<(), String> { for (path, _, _) in self.files.values() { let _ = fs::remove_file(path); // Ignore errors } self.files.clear(); Ok(()) } } #[cfg(test)] mod tests { use super::*; use std::io::Write; #[test] fn test_voice_cache_creation() { let cache = VoiceCache::new(100); assert!(cache.is_ok()); } #[test] fn test_cache_get_nonexistent() { let mut cache = VoiceCache::new(100).unwrap(); assert!(cache.get("nonexistent").is_none()); } #[test] fn test_cache_store_and_get() { let mut cache = VoiceCache::new(100).unwrap(); // Create temporary file let temp_dir = std::env::temp_dir(); let temp_file = temp_dir.join("test_voice.ogg"); let mut file = fs::File::create(&temp_file).unwrap(); file.write_all(b"test audio data").unwrap(); // Store in cache let result = cache.store("test123", &temp_file); assert!(result.is_ok()); // Get from cache let cached_path = cache.get("test123"); assert!(cached_path.is_some()); assert!(cached_path.unwrap().exists()); // Cleanup fs::remove_file(&temp_file).unwrap(); } }