1
//! Code to remove obsolete and extraneous files from a filesystem-based state
2
//! directory.
3

            
4
use std::path::{Path, PathBuf};
5

            
6
use tor_basic_utils::PathExt as _;
7
use tor_error::warn_report;
8
use tracing::warn;
9
use 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.
13
631
fn fname_looks_obsolete(path: &Path) -> bool {
14
631
    if let Some(extension) = path.extension() {
15
629
        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
14
            return true;
19
615
        }
20
2
    }
21

            
22
617
    if let Some(stem) = path.file_stem() {
23
617
        if stem == "default_guards" {
24
            // This file type is obsolete and was removed around 0.0.4.
25
2
            return true;
26
615
        }
27
    }
28

            
29
615
    false
30
631
}
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.
37
const 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.
40
14
fn very_old(entry: &std::fs::DirEntry, now: SystemTime) -> std::io::Result<bool> {
41
14
    Ok(match now.duration_since(entry.metadata()?.modified()?) {
42
12
        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
2
            false
46
        }
47
    })
48
14
}
49

            
50
/// Implementation helper for [`FsStateMgr::clean()`](super::FsStateMgr::clean):
51
/// list all files in `statepath` that are ready to delete as of `now`.
52
607
pub(super) fn files_to_delete(statepath: &Path, now: SystemTime) -> Vec<PathBuf> {
53
607
    let mut result = Vec::new();
54

            
55
608
    let dir_read_failed = |err: std::io::Error| {
56
        use std::io::ErrorKind as EK;
57
2
        match err.kind() {
58
2
            EK::NotFound => {}
59
            _ => warn_report!(
60
                err,
61
                "Failed to scan directory {} for obsolete files",
62
                statepath.display_lossy(),
63
            ),
64
        }
65
2
    };
66
607
    let entries = std::fs::read_dir(statepath)
67
607
        .map_err(dir_read_failed) // Result from fs::read_dir
68
607
        .into_iter()
69
607
        .flatten()
70
644
        .map_while(|result| result.map_err(dir_read_failed).ok()); // Result from dir.next()
71

            
72
1228
    for entry in entries {
73
621
        let path = entry.path();
74
621
        let basename = entry.file_name();
75

            
76
621
        if fname_looks_obsolete(Path::new(&basename)) {
77
10
            match very_old(&entry, now) {
78
6
                Ok(true) => result.push(path),
79
                Ok(false) => {
80
4
                    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
611
        }
94
    }
95

            
96
607
    result
97
607
}
98

            
99
#[cfg(all(test, not(miri) /* filesystem access */))]
100
mod 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
}