tor_persist/fs/
clean.rs

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