Some checks failed
ci/woodpecker/pr/check Pipeline was successful
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
- Add #[allow(unused_imports)] on pub re-exports used only by lib/tests - Add #[allow(dead_code)] on public API items unused in binary target - Fix collapsible_if, redundant_closure, unnecessary_map_or in main.rs - Prefix unused test variables with underscore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
4.6 KiB
Rust
156 lines
4.6 KiB
Rust
//! 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<String, (PathBuf, u64, usize)>,
|
|
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<Self, String> {
|
|
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<PathBuf> {
|
|
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<PathBuf, String> {
|
|
// 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();
|
|
}
|
|
}
|