1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
use std::{
path::{Path, PathBuf},
time::{Duration, SystemTime},
};
use tracing::warn;
fn fname_looks_obsolete(path: &Path) -> bool {
if let Some(extension) = path.extension() {
if extension == "toml" {
return true;
}
}
if let Some(stem) = path.file_stem() {
if stem == "default_guards" {
return true;
}
}
false
}
const CUTOFF: Duration = Duration::from_secs(4 * 24 * 60 * 60);
fn very_old(entry: &std::fs::DirEntry, now: SystemTime) -> std::io::Result<bool> {
Ok(match now.duration_since(entry.metadata()?.modified()?) {
Ok(age) => age > CUTOFF,
Err(_) => {
false
}
})
}
pub(super) fn files_to_delete(statepath: &Path, now: SystemTime) -> Vec<PathBuf> {
let mut result = Vec::new();
let dir_read_failed = |err: std::io::Error| {
use std::io::ErrorKind as EK;
match err.kind() {
EK::NotFound => {}
_ => warn!(
"Failed to scan directory {} for obsolete files: {}",
statepath.display(),
err,
),
}
};
let entries = std::fs::read_dir(statepath)
.map_err(dir_read_failed)
.into_iter()
.flatten()
.map(|result| result.map_err(dir_read_failed).ok())
.take_while(|result| result.is_some())
.flatten();
for entry in entries {
let path = entry.path();
let basename = entry.file_name();
if fname_looks_obsolete(Path::new(&basename)) {
match very_old(&entry, now) {
Ok(true) => result.push(path),
Ok(false) => {
warn!(
"Found obsolete file {}; will delete it when it is older.",
entry.path().display(),
);
}
Err(err) => {
warn!(
"Found obsolete file {} but could not access its modification time: {}",
entry.path().display(),
err,
);
}
}
}
}
result
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn fnames() {
let examples = vec![
("guards", false),
("default_guards.json", true),
("guards.toml", true),
("marzipan.toml", true),
("marzipan.json", false),
];
for (name, obsolete) in examples {
assert_eq!(fname_looks_obsolete(Path::new(name)), obsolete);
}
}
#[test]
fn age() {
let dir = tempfile::TempDir::new().unwrap();
let fname1 = dir.path().join("quokka");
let now = SystemTime::now();
std::fs::write(&fname1, "hello world").unwrap();
let mut r = std::fs::read_dir(dir.path()).unwrap();
let ent = r.next().unwrap().unwrap();
assert!(!very_old(&ent, now).unwrap());
assert!(very_old(&ent, now + CUTOFF * 2).unwrap());
}
#[test]
fn list() {
let dir = tempfile::TempDir::new().unwrap();
let now = SystemTime::now();
let fname1 = dir.path().join("quokka.toml");
std::fs::write(&fname1, "hello world").unwrap();
let fname2 = dir.path().join("wombat.json");
std::fs::write(&fname2, "greetings").unwrap();
let removable_now = files_to_delete(dir.path(), now);
assert!(removable_now.is_empty());
let removable_later = files_to_delete(dir.path(), now + CUTOFF * 2);
assert_eq!(removable_later.len(), 1);
assert_eq!(removable_later[0].file_stem().unwrap(), "quokka");
let removable_earlier = files_to_delete(dir.path(), now - CUTOFF * 2);
assert!(removable_earlier.is_empty());
}
#[test]
fn absent() {
let dir = tempfile::TempDir::new().unwrap();
let dir2 = dir.path().join("subdir_that_doesnt_exist");
let r = files_to_delete(&dir2, SystemTime::now());
assert!(r.is_empty());
}
}