Skip to main content

tor_persist/fs/
clean.rs

1//! Code to remove obsolete and extraneous files from a filesystem-based state
2//! directory.
3
4use 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
11/// Return true if `path` looks like a filename we'd like to remove from our
12/// state directory.
13fn fname_looks_obsolete(path: &Path) -> bool {
14    if let Some(extension) = path.extension() {
15        if extension == "toml" {
16            // We don't make toml files any more.  We migrated to json because
17            // toml isn't so good for serializing arbitrary objects.
18            return true;
19        }
20    }
21
22    if let Some(stem) = path.file_stem() {
23        if stem == "default_guards" {
24            // This file type is obsolete and was removed around 0.0.4.
25            return true;
26        }
27    }
28
29    false
30}
31
32/// How old must an obsolete-looking file be before we're willing to remove it?
33//
34// TODO: This could someday be configurable, if there are in fact users who want
35// to keep obsolete files around in their state directories for months or years,
36// or who need to get rid of them immediately.
37const CUTOFF: Duration = Duration::from_secs(4 * 24 * 60 * 60);
38
39/// Return true if `entry` is very old relative to `now` and therefore safe to delete.
40fn 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            // If duration_since failed, this file is actually from the future, and so it definitely isn't older than the cutoff.
45            false
46        }
47    })
48}
49
50/// Implementation helper for [`FsStateMgr::clean()`](super::FsStateMgr::clean):
51/// list all files in `statepath` that are ready to delete as of `now`.
52pub(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) // Result from fs::read_dir
68        .into_iter()
69        .flatten()
70        .map_while(|result| result.map_err(dir_read_failed).ok()); // Result from dir.next()
71
72    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) /* filesystem access */))]
100mod test {
101    // @@ begin test lint list maintained by maint/add_warning @@
102    #![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    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
114    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        // Make sure we tolerate files written "in the future"
165        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}