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