putzen_cli/caches/
model.rs1use std::path::PathBuf;
4use std::time::{Duration, SystemTime};
5
6#[derive(Clone, Debug)]
7pub struct Cache {
8 pub label: String,
10 pub path: PathBuf,
12 pub size_bytes: u64,
13 pub newest_mtime: Option<SystemTime>,
14 pub file_count: u64,
15 pub dir_count: u64,
16 pub top_files: Vec<TopFile>,
19 pub unreadable: u64,
21}
22
23#[derive(Clone, Debug)]
24pub struct TopFile {
25 pub name: String,
26 pub size_bytes: u64,
27 pub mtime: Option<SystemTime>,
28}
29
30impl Cache {
31 pub fn age(&self, now: SystemTime) -> Option<Duration> {
33 let mtime = self.newest_mtime?;
34 now.duration_since(mtime).ok().or(Some(Duration::ZERO))
35 }
36
37 pub fn score(&self, now: SystemTime) -> f64 {
39 let Some(age) = self.age(now) else { return 0.0 };
40 let mb = self.size_bytes as f64 / 1_048_576.0;
41 let days = age.as_secs_f64() / 86_400.0;
42 mb * days
43 }
44}
45
46#[derive(Copy, Clone, Eq, PartialEq, Debug)]
47pub enum Sort {
48 Score,
49 Size,
50 Age,
51}
52
53impl Sort {
54 pub fn next(self) -> Sort {
55 match self {
56 Sort::Score => Sort::Size,
57 Sort::Size => Sort::Age,
58 Sort::Age => Sort::Score,
59 }
60 }
61}
62
63#[derive(Clone, Debug, Default)]
64pub struct MarkSet {
65 pub marked: std::collections::BTreeSet<usize>,
67}
68
69impl MarkSet {
70 pub fn toggle(&mut self, i: usize) {
71 if !self.marked.insert(i) {
72 self.marked.remove(&i);
73 }
74 }
75 pub fn mark_down_to(&mut self, last: usize) {
76 for i in 0..=last {
77 self.marked.insert(i);
78 }
79 }
80 pub fn clear(&mut self) {
81 self.marked.clear();
82 }
83 pub fn is_marked(&self, i: usize) -> bool {
84 self.marked.contains(&i)
85 }
86 pub fn count(&self) -> usize {
87 self.marked.len()
88 }
89}
90
91#[derive(Copy, Clone, Debug)]
92pub struct FloorPolicy {
93 pub floor: Duration,
94}
95
96impl FloorPolicy {
97 pub fn is_active(&self, age: Option<Duration>) -> bool {
98 age.map(|a| a < self.floor).unwrap_or(false)
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use std::time::{Duration, SystemTime};
106
107 fn at(epoch_secs: u64) -> SystemTime {
108 SystemTime::UNIX_EPOCH + Duration::from_secs(epoch_secs)
109 }
110
111 fn cache(size: u64, mtime_secs: u64) -> Cache {
112 Cache {
113 label: "x".into(),
114 path: PathBuf::from("/tmp/x"),
115 size_bytes: size,
116 newest_mtime: Some(at(mtime_secs)),
117 file_count: 1,
118 dir_count: 0,
119 top_files: Vec::new(),
120 unreadable: 0,
121 }
122 }
123
124 #[test]
125 fn score_zero_for_empty_cache() {
126 let mut c = cache(1024, 0);
127 c.newest_mtime = None;
128 assert_eq!(c.score(at(86_400)), 0.0);
129 }
130
131 #[test]
132 fn score_proportional_to_mb_days() {
133 let now = at(2 * 86_400);
134 let c = cache(1_048_576, 0); assert!((c.score(now) - 2.0).abs() < 1e-6);
136 }
137
138 #[test]
139 fn sort_cycles() {
140 assert_eq!(Sort::Score.next(), Sort::Size);
141 assert_eq!(Sort::Size.next(), Sort::Age);
142 assert_eq!(Sort::Age.next(), Sort::Score);
143 }
144
145 #[test]
146 fn markset_toggle_inserts_and_removes() {
147 let mut m = MarkSet::default();
148 m.toggle(3);
149 assert!(m.is_marked(3));
150 m.toggle(3);
151 assert!(!m.is_marked(3));
152 }
153
154 #[test]
155 fn markset_mark_down_to_inclusive() {
156 let mut m = MarkSet::default();
157 m.mark_down_to(2);
158 assert_eq!(m.count(), 3);
159 for i in 0..=2 {
160 assert!(m.is_marked(i));
161 }
162 }
163
164 #[test]
165 fn floor_active_when_cold_less_than_floor() {
166 let p = FloorPolicy {
167 floor: Duration::from_secs(7 * 86_400),
168 };
169 assert!(p.is_active(Some(Duration::from_secs(86_400))));
170 assert!(!p.is_active(Some(Duration::from_secs(30 * 86_400))));
171 assert!(!p.is_active(None));
172 }
173}