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