use std::path::{Path, PathBuf};
pub trait CacheProvider: Send + Sync {
fn is_model_cached(&self, model_id: &str) -> bool;
fn get_model_path(&self, model_id: &str) -> Option<PathBuf>;
fn cache_dir(&self) -> PathBuf;
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone)]
pub struct FilesystemCacheProvider {
cache_dir: PathBuf,
fixtures_models_dir: Option<PathBuf>,
}
impl Default for FilesystemCacheProvider {
fn default() -> Self {
Self::new()
}
}
impl FilesystemCacheProvider {
pub fn new() -> Self {
let cache_dir = dirs::home_dir()
.map(|h| h.join(".xybrid").join("cache").join("models"))
.unwrap_or_else(|| PathBuf::from(".xybrid/cache/models"));
let fixtures_models_dir = crate::testing::model_fixtures::models_dir().cloned();
Self {
cache_dir,
fixtures_models_dir,
}
}
pub fn with_paths(cache_dir: PathBuf, fixtures_models_dir: Option<PathBuf>) -> Self {
Self {
cache_dir,
fixtures_models_dir,
}
}
fn find_matching_dir(&self, model_id: &str) -> Option<PathBuf> {
let model_id_lower = model_id.to_lowercase();
let model_id_normalized = normalize_name(&model_id_lower);
if let Some(fixtures_dir) = &self.fixtures_models_dir {
let fixtures_path = fixtures_dir.join(model_id);
if fixtures_path.exists() && has_model_files(&fixtures_path) {
return Some(fixtures_path);
}
}
if !self.cache_dir.exists() {
return None;
}
if let Ok(entries) = std::fs::read_dir(&self.cache_dir) {
for entry in entries.flatten() {
let entry_path = entry.path();
if !entry_path.is_dir() {
continue;
}
let dir_name = entry.file_name().to_string_lossy().to_lowercase();
let dir_name_normalized = normalize_name(&dir_name);
let is_match = dir_name.contains(&model_id_lower)
|| dir_name_normalized.contains(&model_id_normalized);
if is_match && has_model_files(&entry_path) {
return Some(entry_path);
}
}
}
None
}
}
impl CacheProvider for FilesystemCacheProvider {
fn is_model_cached(&self, model_id: &str) -> bool {
self.find_matching_dir(model_id).is_some()
}
fn get_model_path(&self, model_id: &str) -> Option<PathBuf> {
self.find_matching_dir(model_id)
}
fn cache_dir(&self) -> PathBuf {
self.cache_dir.clone()
}
fn name(&self) -> &'static str {
"filesystem"
}
}
fn normalize_name(name: &str) -> String {
name.replace(['-', '_', '.'], "")
}
fn has_model_files(path: &Path) -> bool {
path.join("universal.xyb").exists() || path.join("model_metadata.json").exists()
}
#[derive(Debug, Clone, Default)]
pub struct NoopCacheProvider;
impl CacheProvider for NoopCacheProvider {
fn is_model_cached(&self, _model_id: &str) -> bool {
false
}
fn get_model_path(&self, _model_id: &str) -> Option<PathBuf> {
None
}
fn cache_dir(&self) -> PathBuf {
PathBuf::new()
}
fn name(&self) -> &'static str {
"noop"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_name() {
assert_eq!(normalize_name("kokoro-82m"), "kokoro82m");
assert_eq!(normalize_name("kokoro_82m"), "kokoro82m");
assert_eq!(normalize_name("kokoro.82m.v1.0"), "kokoro82mv10");
}
#[test]
fn test_noop_provider() {
let provider = NoopCacheProvider;
assert!(!provider.is_model_cached("any-model"));
assert!(provider.get_model_path("any-model").is_none());
assert_eq!(provider.name(), "noop");
}
#[test]
fn test_filesystem_provider_creation() {
let provider = FilesystemCacheProvider::new();
assert!(provider.cache_dir().to_string_lossy().contains(".xybrid"));
assert_eq!(provider.name(), "filesystem");
}
#[test]
fn test_filesystem_provider_fixtures() {
let provider = FilesystemCacheProvider::new();
let path = provider.get_model_path("kokoro-82m");
if let Some(p) = path {
assert!(p.exists());
}
}
}