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

            
4
use std::{
5
    path::{Path, PathBuf},
6
    time::{Duration, SystemTime},
7
};
8

            
9
use tor_basic_utils::PathExt as _;
10
use tor_error::warn_report;
11
use tracing::warn;
12

            
13
/// Return true if `path` looks like a filename we'd like to remove from our
14
/// state directory.
15
159
fn fname_looks_obsolete(path: &Path) -> bool {
16
159
    if let Some(extension) = path.extension() {
17
157
        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
14
            return true;
21
143
        }
22
2
    }
23

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

            
31
143
    false
32
159
}
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.
39
const 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.
42
14
fn very_old(entry: &std::fs::DirEntry, now: SystemTime) -> std::io::Result<bool> {
43
14
    Ok(match now.duration_since(entry.metadata()?.modified()?) {
44
12
        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
2
            false
48
        }
49
    })
50
14
}
51

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

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

            
74
284
    for entry in entries {
75
149
        let path = entry.path();
76
149
        let basename = entry.file_name();
77
149

            
78
149
        if fname_looks_obsolete(Path::new(&basename)) {
79
10
            match very_old(&entry, now) {
80
6
                Ok(true) => result.push(path),
81
                Ok(false) => {
82
4
                    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
139
        }
96
    }
97

            
98
135
    result
99
135
}
100

            
101
#[cfg(test)]
102
mod 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
}