use crate::cli::arg::{
FochCliCacheArgs, FochCliCacheCleanArgs, FochCliCacheClearArgs, FochCliCacheCommands,
FochCliCacheLayerArg, FochCliCacheListArgs, FochCliCacheStatsArgs,
};
use crate::cli::handler::HandlerResult;
use foch::game::eu4::script::parse_cache;
use foch::platform::cache_store::{
CacheLayerEntryInfo, EvictionStats, FileCacheLayer, cache_cap_bytes, default_foch_cache_dir,
};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Clone, Copy)]
struct CacheLayerSpec {
selection: FochCliCacheLayerArg,
name: &'static str,
extension: &'static str,
}
const CACHE_LAYER_SPECS: [CacheLayerSpec; 5] = [
CacheLayerSpec {
selection: FochCliCacheLayerArg::Mods,
name: "mods",
extension: "rkyv",
},
CacheLayerSpec {
selection: FochCliCacheLayerArg::Diffs,
name: "diffs",
extension: "bin",
},
CacheLayerSpec {
selection: FochCliCacheLayerArg::DagBase,
name: "dag-base",
extension: "bin",
},
CacheLayerSpec {
selection: FochCliCacheLayerArg::CwtRules,
name: "cwt-rules",
extension: "bin",
},
CacheLayerSpec {
selection: FochCliCacheLayerArg::Parse,
name: "parse",
extension: "bin",
},
];
#[derive(Clone, Debug, Default)]
struct LayerStats {
entry_count: usize,
total_bytes: u64,
oldest_mtime: Option<SystemTime>,
newest_mtime: Option<SystemTime>,
}
enum CacheLayer {
Files(FileCacheLayer),
Parse { root: PathBuf },
}
impl CacheLayer {
fn name(&self) -> &str {
match self {
Self::Files(layer) => layer.name(),
Self::Parse { .. } => "parse",
}
}
fn path(&self) -> &Path {
match self {
Self::Files(layer) => layer.path(),
Self::Parse { root } => root,
}
}
fn list_entries(&self) -> Result<Vec<CacheLayerEntryInfo>, Box<dyn std::error::Error>> {
match self {
Self::Files(layer) => Ok(layer.list_entries()?),
Self::Parse { .. } => Ok(parse_cache::list_entries()
.into_iter()
.map(|entry| CacheLayerEntryInfo {
layer_name: "parse".to_string(),
key: entry.key,
path: entry.path,
size_bytes: entry.size_bytes,
modified: entry.modified,
})
.collect()),
}
}
fn purge_older_than(&self, days: u32) -> Result<usize, Box<dyn std::error::Error>> {
match self {
Self::Files(layer) => Ok(layer.purge_older_than(days)?),
Self::Parse { .. } => Ok(parse_cache::purge_older_than(days)?),
}
}
fn evict_to_byte_cap(
&self,
cap_bytes: u64,
) -> Result<EvictionStats, Box<dyn std::error::Error>> {
match self {
Self::Files(layer) => Ok(layer.evict_to_byte_cap(cap_bytes)?),
Self::Parse { .. } => {
let stats = parse_cache::gc_with_cap(cap_bytes);
Ok(EvictionStats {
removed_entries: usize::try_from(stats.evicted).unwrap_or(usize::MAX),
freed_bytes: stats.bytes_before.saturating_sub(stats.bytes_after),
})
}
}
}
fn clear(&self) -> Result<(), Box<dyn std::error::Error>> {
match self {
Self::Files(layer) => Ok(layer.clear()?),
Self::Parse { .. } => Ok(parse_cache::cache_clean()?),
}
}
}
pub fn handle_cache(cache_args: &FochCliCacheArgs) -> HandlerResult {
match &cache_args.command {
FochCliCacheCommands::Stats(args) => handle_cache_stats(args),
FochCliCacheCommands::List(args) => handle_cache_list(args),
FochCliCacheCommands::Clean(args) => handle_cache_clean(args),
FochCliCacheCommands::Clear(args) => handle_cache_clear(args),
FochCliCacheCommands::Where => handle_cache_where(),
}
}
fn handle_cache_stats(args: &FochCliCacheStatsArgs) -> HandlerResult {
let root = default_foch_cache_dir();
let layers = selected_layers(args.layer);
let mut total_size = 0_u64;
let mut total_entries = 0_usize;
let mut oldest = None;
let mut newest = None;
println!("cache root: {}", root.display());
println!("layer entries size path");
for layer in &layers {
let stats = layer_stats(layer)?;
total_size = total_size.saturating_add(stats.total_bytes);
total_entries = total_entries.saturating_add(stats.entry_count);
oldest = min_time(oldest, stats.oldest_mtime);
newest = max_time(newest, stats.newest_mtime);
println!(
"{:<11} {:>7} {:>9} {}",
layer.name(),
stats.entry_count,
format_bytes(stats.total_bytes),
layer.path().display()
);
}
println!(
"total: {:>7} {:>9}",
total_entries,
format_bytes(total_size)
);
println!("oldest: {}", format_optional_time(oldest));
println!("newest: {}", format_optional_time(newest));
Ok(0)
}
fn handle_cache_list(args: &FochCliCacheListArgs) -> HandlerResult {
let layers = selected_layers(args.layer);
let mut entries = Vec::new();
for layer in &layers {
entries.extend(layer.list_entries()?);
}
entries.sort_by(|left, right| {
left.layer_name
.cmp(&right.layer_name)
.then_with(|| left.modified.cmp(&right.modified))
.then_with(|| left.key.cmp(&right.key))
});
println!("layer modified size key");
for entry in entries {
println!(
"{:<11} {:<32} {:>9} {}",
entry.layer_name,
format_system_time(entry.modified),
format_bytes(entry.size_bytes),
entry.key
);
}
Ok(0)
}
fn handle_cache_clean(args: &FochCliCacheCleanArgs) -> HandlerResult {
let layers = selected_layers(args.layer);
let mut purged = 0_usize;
for layer in &layers {
purged += layer.purge_older_than(args.older_than)?;
}
println!(
"removed: {purged} entries older than {} days",
args.older_than
);
let cap = args.byte_cap.unwrap_or_else(cache_cap_bytes);
let mut evicted_entries = 0_usize;
let mut freed_bytes = 0_u64;
for layer in &layers {
let stats = layer.evict_to_byte_cap(cap)?;
evicted_entries = evicted_entries.saturating_add(stats.removed_entries);
freed_bytes = freed_bytes.saturating_add(stats.freed_bytes);
}
println!(
"byte-cap: evicted {} files ({}) with cap {}",
evicted_entries,
format_bytes(freed_bytes),
format_bytes(cap)
);
Ok(0)
}
fn handle_cache_clear(args: &FochCliCacheClearArgs) -> HandlerResult {
if !args.yes {
return Err("refusing to clear cache without --yes".into());
}
let layers = selected_layers(args.layer);
let stats = combined_stats(&layers)?;
for layer in &layers {
layer.clear()?;
}
println!(
"removed: {} entries ({}) from {}",
stats.entry_count,
format_bytes(stats.total_bytes),
format_layer_selection(args.layer)
);
Ok(0)
}
fn handle_cache_where() -> HandlerResult {
println!("{}", default_foch_cache_dir().display());
Ok(0)
}
fn selected_layers(arg: Option<FochCliCacheLayerArg>) -> Vec<CacheLayer> {
let selected = arg.unwrap_or(FochCliCacheLayerArg::All);
let root = default_foch_cache_dir();
CACHE_LAYER_SPECS
.iter()
.filter(|spec| selected == FochCliCacheLayerArg::All || spec.selection == selected)
.map(|spec| {
if spec.selection == FochCliCacheLayerArg::Parse {
CacheLayer::Parse {
root: root.join(spec.name),
}
} else {
CacheLayer::Files(FileCacheLayer::new(
spec.name,
root.join(spec.name),
spec.extension,
))
}
})
.collect()
}
fn layer_stats(layer: &CacheLayer) -> Result<LayerStats, Box<dyn std::error::Error>> {
let entries = layer.list_entries()?;
Ok(stats_from_entries(&entries))
}
fn combined_stats(layers: &[CacheLayer]) -> Result<LayerStats, Box<dyn std::error::Error>> {
let mut entries = Vec::new();
for layer in layers {
entries.extend(layer.list_entries()?);
}
Ok(stats_from_entries(&entries))
}
fn stats_from_entries(entries: &[CacheLayerEntryInfo]) -> LayerStats {
let mut stats = LayerStats {
entry_count: entries.len(),
total_bytes: entries.iter().map(|entry| entry.size_bytes).sum(),
oldest_mtime: None,
newest_mtime: None,
};
for entry in entries {
stats.oldest_mtime = min_time(stats.oldest_mtime, Some(entry.modified));
stats.newest_mtime = max_time(stats.newest_mtime, Some(entry.modified));
}
stats
}
fn min_time(current: Option<SystemTime>, candidate: Option<SystemTime>) -> Option<SystemTime> {
match (current, candidate) {
(Some(current), Some(candidate)) => Some(current.min(candidate)),
(None, Some(candidate)) => Some(candidate),
(Some(current), None) => Some(current),
(None, None) => None,
}
}
fn max_time(current: Option<SystemTime>, candidate: Option<SystemTime>) -> Option<SystemTime> {
match (current, candidate) {
(Some(current), Some(candidate)) => Some(current.max(candidate)),
(None, Some(candidate)) => Some(candidate),
(Some(current), None) => Some(current),
(None, None) => None,
}
}
fn format_layer_selection(layer: Option<FochCliCacheLayerArg>) -> String {
match layer.unwrap_or(FochCliCacheLayerArg::All) {
FochCliCacheLayerArg::All => default_foch_cache_dir().display().to_string(),
other => selected_layers(Some(other))
.into_iter()
.next()
.map(|layer| layer.path().display().to_string())
.unwrap_or_else(|| default_foch_cache_dir().display().to_string()),
}
}
fn format_bytes(bytes: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
if bytes < 1024 {
return format!("{bytes} B");
}
let mut value = bytes as f64;
let mut unit = 0_usize;
while value >= 1024.0 && unit < UNITS.len() - 1 {
value /= 1024.0;
unit += 1;
}
format!("{value:.1} {}", UNITS[unit])
}
fn format_optional_time(time: Option<SystemTime>) -> String {
match time {
Some(time) => format_system_time(time),
None => "n/a".to_string(),
}
}
fn format_system_time(time: SystemTime) -> String {
let timestamp = match time.duration_since(UNIX_EPOCH) {
Ok(duration) => duration.as_secs() as i64,
Err(err) => -(err.duration().as_secs() as i64),
};
let days = timestamp.div_euclid(86_400);
let seconds_of_day = timestamp.rem_euclid(86_400);
let hour = seconds_of_day / 3_600;
let minute = (seconds_of_day % 3_600) / 60;
let second = seconds_of_day % 60;
let (year, month, day) = civil_from_days(days);
format!(
"{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02} UTC ({})",
format_age(time)
)
}
fn civil_from_days(days_since_unix_epoch: i64) -> (i64, u32, u32) {
let z = days_since_unix_epoch + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = mp + if mp < 10 { 3 } else { -9 };
let year = y + if m <= 2 { 1 } else { 0 };
(year, m as u32, d as u32)
}
fn format_age(time: SystemTime) -> String {
match SystemTime::now().duration_since(time) {
Ok(age) => format_duration_ago(age),
Err(err) => format!("{} in the future", format_duration(err.duration())),
}
}
fn format_duration_ago(duration: Duration) -> String {
if duration < Duration::from_secs(5) {
"just now".to_string()
} else {
format!("{} ago", format_duration(duration))
}
}
fn format_duration(duration: Duration) -> String {
let seconds = duration.as_secs();
if seconds < 60 {
format!("{seconds}s")
} else if seconds < 3_600 {
format!("{} min", seconds / 60)
} else if seconds < 86_400 {
format!("{} h", seconds / 3_600)
} else {
format!("{} days", seconds / 86_400)
}
}