use crate::error::{FileSystemError, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
const CACHE_DIR: &str = ".vika-cache";
fn cache_key(spec_path: &str, spec_name: Option<&str>) -> String {
if let Some(name) = spec_name {
format!("{}:{}", name, spec_path)
} else {
spec_path.to_string()
}
}
fn cache_file_names(cache_key: &str) -> (String, String) {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
cache_key.hash(&mut hasher);
let hash = format!("{:x}", hasher.finish());
(
format!("spec_{}.json", hash),
format!("spec_{}.meta.json", hash),
)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SpecMetadata {
pub url: String,
pub timestamp: u64,
pub etag: Option<String>,
pub content_hash: String,
}
pub struct CacheManager;
impl CacheManager {
pub fn ensure_cache_dir() -> Result<PathBuf> {
let cache_dir = PathBuf::from(CACHE_DIR);
std::fs::create_dir_all(&cache_dir).map_err(|e| {
FileSystemError::CreateDirectoryFailed {
path: CACHE_DIR.to_string(),
source: e,
}
})?;
Ok(cache_dir)
}
pub fn get_cached_spec(url: &str) -> Result<Option<String>> {
Self::get_cached_spec_with_name(url, None)
}
pub fn get_cached_spec_with_name(url: &str, spec_name: Option<&str>) -> Result<Option<String>> {
let cache_dir = Self::ensure_cache_dir()?;
let key = cache_key(url, spec_name);
let (spec_file, meta_file) = cache_file_names(&key);
let meta_path = cache_dir.join(&meta_file);
let spec_path = cache_dir.join(&spec_file);
if !meta_path.exists() || !spec_path.exists() {
return Ok(None);
}
let meta_content =
std::fs::read_to_string(&meta_path).map_err(|e| FileSystemError::ReadFileFailed {
path: meta_path.display().to_string(),
source: e,
})?;
let metadata: SpecMetadata =
serde_json::from_str(&meta_content).map_err(|_| FileSystemError::ReadFileFailed {
path: meta_path.display().to_string(),
source: std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid metadata format",
),
})?;
if metadata.url != url {
return Ok(None);
}
let spec_content =
std::fs::read_to_string(&spec_path).map_err(|e| FileSystemError::ReadFileFailed {
path: spec_path.display().to_string(),
source: e,
})?;
Ok(Some(spec_content))
}
pub fn cache_spec(url: &str, content: &str) -> Result<()> {
Self::cache_spec_with_name(url, content, None)
}
pub fn cache_spec_with_name(url: &str, content: &str, spec_name: Option<&str>) -> Result<()> {
let cache_dir = Self::ensure_cache_dir()?;
let key = cache_key(url, spec_name);
let (spec_file, meta_file) = cache_file_names(&key);
let meta_path = cache_dir.join(&meta_file);
let spec_path = cache_dir.join(&spec_file);
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
let content_hash = format!("{:x}", hasher.finish());
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let metadata = SpecMetadata {
url: url.to_string(),
timestamp,
etag: None, content_hash,
};
let meta_json = serde_json::to_string_pretty(&metadata).map_err(|e| {
FileSystemError::WriteFileFailed {
path: meta_path.display().to_string(),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{}", e)),
}
})?;
std::fs::write(&meta_path, meta_json).map_err(|e| FileSystemError::WriteFileFailed {
path: meta_path.display().to_string(),
source: e,
})?;
std::fs::write(&spec_path, content).map_err(|e| FileSystemError::WriteFileFailed {
path: spec_path.display().to_string(),
source: e,
})?;
Ok(())
}
pub fn clear_cache() -> Result<()> {
let cache_dir = PathBuf::from(CACHE_DIR);
if cache_dir.exists() {
std::fs::remove_dir_all(&cache_dir).map_err(|e| {
FileSystemError::CreateDirectoryFailed {
path: CACHE_DIR.to_string(),
source: e,
}
})?;
}
Ok(())
}
}