use crate::api::middleware::Middleware;
use serde::{Deserialize, Serialize};
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use dirs_2;
use thiserror::Error;
#[derive(Error, Debug)]
pub(crate) enum CacheError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("JSON serialization error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Cache error: {0}")]
GeneralError(String),
}
#[derive(Debug, Serialize, Deserialize)]
struct CacheEntry<Res> {
result: Res,
timestamp: u64,
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub max_age_seconds: Option<u64>,
pub enabled: bool,
pub skip_reads: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self::from_env()
}
}
impl CacheConfig {
pub fn from_env() -> Self {
Self::from_env_with_prefix(None)
}
pub fn from_env_with_prefix(api_prefix: Option<&str>) -> Self {
Self::from_env_with_prefix_and_default(api_prefix, Some(24 * 60 * 60)) }
pub fn from_env_with_prefix_and_default(api_prefix: Option<&str>, default_max_age: Option<u64>) -> Self {
let enabled = api_prefix
.and_then(|prefix| Self::parse_env_bool(&format!("UVM_LIVE_PLATFORM_{}_CACHE_ENABLED", prefix)))
.or_else(|| Self::parse_env_bool("UVM_LIVE_PLATFORM_CACHE_ENABLED"))
.unwrap_or(true);
let max_age_seconds = api_prefix
.and_then(|prefix| Self::parse_env_duration(&format!("UVM_LIVE_PLATFORM_{}_CACHE_MAX_AGE_SECONDS", prefix)))
.or_else(|| Self::parse_env_duration("UVM_LIVE_PLATFORM_CACHE_MAX_AGE_SECONDS"))
.unwrap_or(default_max_age);
Self {
enabled,
max_age_seconds,
skip_reads: false,
}
}
fn parse_env_bool(key: &str) -> Option<bool> {
std::env::var(key).ok().and_then(|v| {
match v.to_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Some(true),
"false" | "0" | "no" | "off" => Some(false),
_ => None,
}
})
}
fn parse_env_duration(key: &str) -> Option<Option<u64>> {
std::env::var(key).ok().and_then(|v| {
let v = v.trim();
if v.to_lowercase() == "never" || v.to_lowercase() == "none" {
return Some(None); }
if let Ok(duration) = humantime::parse_duration(v) {
return Some(Some(duration.as_secs()));
}
v.parse::<u64>().ok().map(Some)
})
}
}
#[derive(Debug, Clone)]
pub struct Cache<Opts, Res> {
config: CacheConfig,
_phantom: std::marker::PhantomData<(Opts, Res)>,
}
impl<Opts, Res> Cache<Opts, Res>
where
Opts: Hash + Serialize + for<'de> Deserialize<'de>,
Res: Clone + Serialize + for<'de> Deserialize<'de>,
{
pub fn new(config: CacheConfig) -> Self {
Self {
config,
_phantom: std::marker::PhantomData,
}
}
pub fn default() -> Self {
Self::new(CacheConfig::default())
}
pub fn disabled() -> Self {
Self::new(CacheConfig {
enabled: false,
max_age_seconds: None,
skip_reads: false,
})
}
pub fn refresh_mode() -> Self {
Self::new(CacheConfig {
enabled: true,
max_age_seconds: Some(24 * 60 * 60), skip_reads: true,
})
}
fn generate_cache_key(options: &Opts) -> String {
let mut hasher = DefaultHasher::new();
options.hash(&mut hasher);
format!("cache_{:x}", hasher.finish())
}
fn cache_dir() -> Result<PathBuf, std::io::Error> {
dirs_2::cache_dir()
.map(|path| path.join("com.github.larusso.unity-version-manager").join("cache"))
.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "Unable to determine cache directory")
})
}
fn cache_file_path(options: &Opts) -> Result<PathBuf, CacheError> {
let cache_key = Self::generate_cache_key(options);
let cache_dir = Self::cache_dir().map_err(|e| CacheError::GeneralError(format!("Unable to determine cache directory: {}", e)))?;
Ok(cache_dir.join(format!("{}.json", cache_key)))
}
fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn is_cache_valid(&self, entry: &CacheEntry<Res>) -> bool {
if let Some(max_age) = self.config.max_age_seconds {
let current_time = Self::current_timestamp();
current_time.saturating_sub(entry.timestamp) <= max_age
} else {
true
}
}
fn get(&self, options: &Opts) -> Result<Option<Res>, CacheError> {
if !self.config.enabled || self.config.skip_reads {
return Ok(None);
}
let cache_file = Self::cache_file_path(options)?;
if !cache_file.exists() {
return Ok(None);
}
let contents = fs::read_to_string(&cache_file)?;
let entry: CacheEntry<Res> = serde_json::from_str(&contents)?;
if self.is_cache_valid(&entry) {
Ok(Some(entry.result))
} else {
let _ = fs::remove_file(&cache_file);
Ok(None)
}
}
fn put(&self, options: &Opts, result: Res) -> Result<(), CacheError> {
if !self.config.enabled {
return Ok(());
}
let cache_file = Self::cache_file_path(options)?;
if let Some(parent) = cache_file.parent() {
fs::create_dir_all(parent)?;
}
let entry = CacheEntry {
result,
timestamp: Self::current_timestamp(),
};
let serialized = serde_json::to_string_pretty(&entry)?;
fs::write(&cache_file, serialized)?;
Ok(())
}
fn clear(&self) -> Result<(), CacheError> {
let cache_dir = Self::cache_dir().map_err(|e| CacheError::GeneralError(format!("Unable to determine cache directory: {}", e)))?;
if cache_dir.exists() {
fs::remove_dir_all(&cache_dir)?;
}
Ok(())
}
fn stats(&self) -> Result<CacheStats, CacheError> {
let cache_dir = Self::cache_dir().map_err(|e| CacheError::GeneralError(format!("Unable to determine cache directory: {}", e)))?;
if !cache_dir.exists() {
return Ok(CacheStats { total_entries: 0, valid_entries: 0 });
}
let entries = fs::read_dir(&cache_dir)?;
let mut total_entries = 0;
let mut valid_entries = 0;
for entry in entries {
if let Ok(entry) = entry {
if let Some(extension) = entry.path().extension() {
if extension == "json" {
total_entries += 1;
if let Ok(contents) = fs::read_to_string(entry.path()) {
if let Ok(cache_entry) = serde_json::from_str::<CacheEntry<Res>>(&contents) {
if self.is_cache_valid(&cache_entry) {
valid_entries += 1;
}
}
}
}
}
}
}
Ok(CacheStats { total_entries, valid_entries })
}
}
#[derive(Debug)]
pub struct CacheStats {
pub total_entries: usize,
pub valid_entries: usize,
}
#[derive(Debug, Clone)]
pub struct CacheMiddleware<Opts, Res> {
cache: Cache<Opts, Res>,
}
impl<Opts, Res> CacheMiddleware<Opts, Res>
where
Opts: Hash + Serialize + for<'de> Deserialize<'de>,
Res: Clone + Serialize + for<'de> Deserialize<'de>,
{
pub fn new(config: CacheConfig) -> Self {
Self {
cache: Cache::new(config),
}
}
pub fn refresh_mode() -> Self {
Self {
cache: Cache::refresh_mode(),
}
}
pub fn default() -> Self {
Self {
cache: Cache::default(),
}
}
pub fn disabled() -> Self {
Self {
cache: Cache::disabled(),
}
}
pub fn cache(&self) -> &Cache<Opts, Res> {
&self.cache
}
}
impl<Opts, Res, Err> Middleware<Opts, Res, Err> for CacheMiddleware<Opts, Res>
where
Opts: Hash + Serialize + for<'de> Deserialize<'de>,
Res: Clone + Serialize + for<'de> Deserialize<'de>,
Err: From<CacheError>,
{
fn process(
&self,
options: &Opts,
next: &dyn Fn(&Opts) -> Result<Res, Err>,
) -> Result<Res, Err> {
if let Ok(Some(cached_result)) = self.cache.get(options) {
return Ok(cached_result);
}
let result = next(options)?;
let _ = self.cache.put(options, result.clone());
Ok(result)
}
}