Skip to main content

foch_cli/cli/handler/
cache.rs

1use crate::cli::arg::{
2	FochCliCacheArgs, FochCliCacheCleanArgs, FochCliCacheClearArgs, FochCliCacheCommands,
3	FochCliCacheLayerArg, FochCliCacheListArgs, FochCliCacheStatsArgs,
4};
5use crate::cli::handler::HandlerResult;
6use foch::game::eu4::script::parse_cache;
7use foch::platform::cache_store::{
8	CacheLayerEntryInfo, EvictionStats, FileCacheLayer, cache_cap_bytes, default_foch_cache_dir,
9};
10use std::path::{Path, PathBuf};
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13#[derive(Clone, Copy)]
14struct CacheLayerSpec {
15	selection: FochCliCacheLayerArg,
16	name: &'static str,
17	extension: &'static str,
18}
19
20const CACHE_LAYER_SPECS: [CacheLayerSpec; 5] = [
21	CacheLayerSpec {
22		selection: FochCliCacheLayerArg::Mods,
23		name: "mods",
24		extension: "rkyv",
25	},
26	CacheLayerSpec {
27		selection: FochCliCacheLayerArg::Diffs,
28		name: "diffs",
29		extension: "bin",
30	},
31	CacheLayerSpec {
32		selection: FochCliCacheLayerArg::DagBase,
33		name: "dag-base",
34		extension: "bin",
35	},
36	CacheLayerSpec {
37		selection: FochCliCacheLayerArg::CwtRules,
38		name: "cwt-rules",
39		extension: "bin",
40	},
41	CacheLayerSpec {
42		selection: FochCliCacheLayerArg::Parse,
43		name: "parse",
44		extension: "bin",
45	},
46];
47
48#[derive(Clone, Debug, Default)]
49struct LayerStats {
50	entry_count: usize,
51	total_bytes: u64,
52	oldest_mtime: Option<SystemTime>,
53	newest_mtime: Option<SystemTime>,
54}
55
56enum CacheLayer {
57	Files(FileCacheLayer),
58	Parse { root: PathBuf },
59}
60
61impl CacheLayer {
62	fn name(&self) -> &str {
63		match self {
64			Self::Files(layer) => layer.name(),
65			Self::Parse { .. } => "parse",
66		}
67	}
68
69	fn path(&self) -> &Path {
70		match self {
71			Self::Files(layer) => layer.path(),
72			Self::Parse { root } => root,
73		}
74	}
75
76	fn list_entries(&self) -> Result<Vec<CacheLayerEntryInfo>, Box<dyn std::error::Error>> {
77		match self {
78			Self::Files(layer) => Ok(layer.list_entries()?),
79			Self::Parse { .. } => Ok(parse_cache::list_entries()
80				.into_iter()
81				.map(|entry| CacheLayerEntryInfo {
82					layer_name: "parse".to_string(),
83					key: entry.key,
84					path: entry.path,
85					size_bytes: entry.size_bytes,
86					modified: entry.modified,
87				})
88				.collect()),
89		}
90	}
91
92	fn purge_older_than(&self, days: u32) -> Result<usize, Box<dyn std::error::Error>> {
93		match self {
94			Self::Files(layer) => Ok(layer.purge_older_than(days)?),
95			Self::Parse { .. } => Ok(parse_cache::purge_older_than(days)?),
96		}
97	}
98
99	fn evict_to_byte_cap(
100		&self,
101		cap_bytes: u64,
102	) -> Result<EvictionStats, Box<dyn std::error::Error>> {
103		match self {
104			Self::Files(layer) => Ok(layer.evict_to_byte_cap(cap_bytes)?),
105			Self::Parse { .. } => {
106				let stats = parse_cache::gc_with_cap(cap_bytes);
107				Ok(EvictionStats {
108					removed_entries: usize::try_from(stats.evicted).unwrap_or(usize::MAX),
109					freed_bytes: stats.bytes_before.saturating_sub(stats.bytes_after),
110				})
111			}
112		}
113	}
114
115	fn clear(&self) -> Result<(), Box<dyn std::error::Error>> {
116		match self {
117			Self::Files(layer) => Ok(layer.clear()?),
118			Self::Parse { .. } => Ok(parse_cache::cache_clean()?),
119		}
120	}
121}
122
123pub fn handle_cache(cache_args: &FochCliCacheArgs) -> HandlerResult {
124	match &cache_args.command {
125		FochCliCacheCommands::Stats(args) => handle_cache_stats(args),
126		FochCliCacheCommands::List(args) => handle_cache_list(args),
127		FochCliCacheCommands::Clean(args) => handle_cache_clean(args),
128		FochCliCacheCommands::Clear(args) => handle_cache_clear(args),
129		FochCliCacheCommands::Where => handle_cache_where(),
130	}
131}
132
133fn handle_cache_stats(args: &FochCliCacheStatsArgs) -> HandlerResult {
134	let root = default_foch_cache_dir();
135	let layers = selected_layers(args.layer);
136	let mut total_size = 0_u64;
137	let mut total_entries = 0_usize;
138	let mut oldest = None;
139	let mut newest = None;
140
141	println!("cache root: {}", root.display());
142	println!("layer       entries  size       path");
143	for layer in &layers {
144		let stats = layer_stats(layer)?;
145		total_size = total_size.saturating_add(stats.total_bytes);
146		total_entries = total_entries.saturating_add(stats.entry_count);
147		oldest = min_time(oldest, stats.oldest_mtime);
148		newest = max_time(newest, stats.newest_mtime);
149		println!(
150			"{:<11} {:>7}  {:>9}  {}",
151			layer.name(),
152			stats.entry_count,
153			format_bytes(stats.total_bytes),
154			layer.path().display()
155		);
156	}
157	println!(
158		"total:      {:>7}  {:>9}",
159		total_entries,
160		format_bytes(total_size)
161	);
162	println!("oldest:     {}", format_optional_time(oldest));
163	println!("newest:     {}", format_optional_time(newest));
164	Ok(0)
165}
166
167fn handle_cache_list(args: &FochCliCacheListArgs) -> HandlerResult {
168	let layers = selected_layers(args.layer);
169	let mut entries = Vec::new();
170	for layer in &layers {
171		entries.extend(layer.list_entries()?);
172	}
173	entries.sort_by(|left, right| {
174		left.layer_name
175			.cmp(&right.layer_name)
176			.then_with(|| left.modified.cmp(&right.modified))
177			.then_with(|| left.key.cmp(&right.key))
178	});
179
180	println!("layer       modified                         size       key");
181	for entry in entries {
182		println!(
183			"{:<11} {:<32} {:>9}  {}",
184			entry.layer_name,
185			format_system_time(entry.modified),
186			format_bytes(entry.size_bytes),
187			entry.key
188		);
189	}
190	Ok(0)
191}
192
193fn handle_cache_clean(args: &FochCliCacheCleanArgs) -> HandlerResult {
194	let layers = selected_layers(args.layer);
195
196	let mut purged = 0_usize;
197	for layer in &layers {
198		purged += layer.purge_older_than(args.older_than)?;
199	}
200	println!(
201		"removed: {purged} entries older than {} days",
202		args.older_than
203	);
204
205	let cap = args.byte_cap.unwrap_or_else(cache_cap_bytes);
206	let mut evicted_entries = 0_usize;
207	let mut freed_bytes = 0_u64;
208	for layer in &layers {
209		let stats = layer.evict_to_byte_cap(cap)?;
210		evicted_entries = evicted_entries.saturating_add(stats.removed_entries);
211		freed_bytes = freed_bytes.saturating_add(stats.freed_bytes);
212	}
213	println!(
214		"byte-cap: evicted {} files ({}) with cap {}",
215		evicted_entries,
216		format_bytes(freed_bytes),
217		format_bytes(cap)
218	);
219	Ok(0)
220}
221
222fn handle_cache_clear(args: &FochCliCacheClearArgs) -> HandlerResult {
223	if !args.yes {
224		return Err("refusing to clear cache without --yes".into());
225	}
226	let layers = selected_layers(args.layer);
227	let stats = combined_stats(&layers)?;
228	for layer in &layers {
229		layer.clear()?;
230	}
231	println!(
232		"removed: {} entries ({}) from {}",
233		stats.entry_count,
234		format_bytes(stats.total_bytes),
235		format_layer_selection(args.layer)
236	);
237	Ok(0)
238}
239
240fn handle_cache_where() -> HandlerResult {
241	println!("{}", default_foch_cache_dir().display());
242	Ok(0)
243}
244
245fn selected_layers(arg: Option<FochCliCacheLayerArg>) -> Vec<CacheLayer> {
246	let selected = arg.unwrap_or(FochCliCacheLayerArg::All);
247	let root = default_foch_cache_dir();
248	CACHE_LAYER_SPECS
249		.iter()
250		.filter(|spec| selected == FochCliCacheLayerArg::All || spec.selection == selected)
251		.map(|spec| {
252			if spec.selection == FochCliCacheLayerArg::Parse {
253				CacheLayer::Parse {
254					root: root.join(spec.name),
255				}
256			} else {
257				CacheLayer::Files(FileCacheLayer::new(
258					spec.name,
259					root.join(spec.name),
260					spec.extension,
261				))
262			}
263		})
264		.collect()
265}
266
267fn layer_stats(layer: &CacheLayer) -> Result<LayerStats, Box<dyn std::error::Error>> {
268	let entries = layer.list_entries()?;
269	Ok(stats_from_entries(&entries))
270}
271
272fn combined_stats(layers: &[CacheLayer]) -> Result<LayerStats, Box<dyn std::error::Error>> {
273	let mut entries = Vec::new();
274	for layer in layers {
275		entries.extend(layer.list_entries()?);
276	}
277	Ok(stats_from_entries(&entries))
278}
279
280fn stats_from_entries(entries: &[CacheLayerEntryInfo]) -> LayerStats {
281	let mut stats = LayerStats {
282		entry_count: entries.len(),
283		total_bytes: entries.iter().map(|entry| entry.size_bytes).sum(),
284		oldest_mtime: None,
285		newest_mtime: None,
286	};
287	for entry in entries {
288		stats.oldest_mtime = min_time(stats.oldest_mtime, Some(entry.modified));
289		stats.newest_mtime = max_time(stats.newest_mtime, Some(entry.modified));
290	}
291	stats
292}
293
294fn min_time(current: Option<SystemTime>, candidate: Option<SystemTime>) -> Option<SystemTime> {
295	match (current, candidate) {
296		(Some(current), Some(candidate)) => Some(current.min(candidate)),
297		(None, Some(candidate)) => Some(candidate),
298		(Some(current), None) => Some(current),
299		(None, None) => None,
300	}
301}
302
303fn max_time(current: Option<SystemTime>, candidate: Option<SystemTime>) -> Option<SystemTime> {
304	match (current, candidate) {
305		(Some(current), Some(candidate)) => Some(current.max(candidate)),
306		(None, Some(candidate)) => Some(candidate),
307		(Some(current), None) => Some(current),
308		(None, None) => None,
309	}
310}
311
312fn format_layer_selection(layer: Option<FochCliCacheLayerArg>) -> String {
313	match layer.unwrap_or(FochCliCacheLayerArg::All) {
314		FochCliCacheLayerArg::All => default_foch_cache_dir().display().to_string(),
315		other => selected_layers(Some(other))
316			.into_iter()
317			.next()
318			.map(|layer| layer.path().display().to_string())
319			.unwrap_or_else(|| default_foch_cache_dir().display().to_string()),
320	}
321}
322
323fn format_bytes(bytes: u64) -> String {
324	const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
325	if bytes < 1024 {
326		return format!("{bytes} B");
327	}
328
329	let mut value = bytes as f64;
330	let mut unit = 0_usize;
331	while value >= 1024.0 && unit < UNITS.len() - 1 {
332		value /= 1024.0;
333		unit += 1;
334	}
335	format!("{value:.1} {}", UNITS[unit])
336}
337
338fn format_optional_time(time: Option<SystemTime>) -> String {
339	match time {
340		Some(time) => format_system_time(time),
341		None => "n/a".to_string(),
342	}
343}
344
345fn format_system_time(time: SystemTime) -> String {
346	let timestamp = match time.duration_since(UNIX_EPOCH) {
347		Ok(duration) => duration.as_secs() as i64,
348		Err(err) => -(err.duration().as_secs() as i64),
349	};
350	let days = timestamp.div_euclid(86_400);
351	let seconds_of_day = timestamp.rem_euclid(86_400);
352	let hour = seconds_of_day / 3_600;
353	let minute = (seconds_of_day % 3_600) / 60;
354	let second = seconds_of_day % 60;
355	let (year, month, day) = civil_from_days(days);
356	format!(
357		"{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02} UTC ({})",
358		format_age(time)
359	)
360}
361
362fn civil_from_days(days_since_unix_epoch: i64) -> (i64, u32, u32) {
363	let z = days_since_unix_epoch + 719_468;
364	let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
365	let doe = z - era * 146_097;
366	let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
367	let y = yoe + era * 400;
368	let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
369	let mp = (5 * doy + 2) / 153;
370	let d = doy - (153 * mp + 2) / 5 + 1;
371	let m = mp + if mp < 10 { 3 } else { -9 };
372	let year = y + if m <= 2 { 1 } else { 0 };
373	(year, m as u32, d as u32)
374}
375
376fn format_age(time: SystemTime) -> String {
377	match SystemTime::now().duration_since(time) {
378		Ok(age) => format_duration_ago(age),
379		Err(err) => format!("{} in the future", format_duration(err.duration())),
380	}
381}
382
383fn format_duration_ago(duration: Duration) -> String {
384	if duration < Duration::from_secs(5) {
385		"just now".to_string()
386	} else {
387		format!("{} ago", format_duration(duration))
388	}
389}
390
391fn format_duration(duration: Duration) -> String {
392	let seconds = duration.as_secs();
393	if seconds < 60 {
394		format!("{seconds}s")
395	} else if seconds < 3_600 {
396		format!("{} min", seconds / 60)
397	} else if seconds < 86_400 {
398		format!("{} h", seconds / 3_600)
399	} else {
400		format!("{} days", seconds / 86_400)
401	}
402}