stor_age/analysis/
universal.rs

1use std::collections::HashMap;
2use std::fs::{self, ReadDir};
3use std::io::ErrorKind;
4use std::path::Path;
5use std::time::{Duration, SystemTime};
6
7#[cfg(target_family = "unix")]
8use std::os::unix::fs::MetadataExt;
9
10use anyhow::Result;
11
12use crate::Data;
13
14/// Runs universal directory traversal.
15///
16/// # Errors
17///
18/// - walking directory
19/// - reading inode metadata
20pub fn run(
21    dir: &str,
22    ages_in_days: &[u64],
23    // ALLOW not needed on non-unix platforms
24    #[allow(unused_variables)] one_file_system: bool,
25) -> Result<Data> {
26    let thresholds = thresholds(ages_in_days);
27
28    #[cfg(target_family = "unix")]
29    let dev = if one_file_system {
30        Some(fs::metadata(dir)?.dev())
31    } else {
32        None
33    };
34
35    #[cfg(not(target_family = "unix"))]
36    let dev = None;
37
38    walk(Path::new(dir), &thresholds, ages_in_days, dev)
39}
40
41fn thresholds(ages_in_days: &[u64]) -> HashMap<u64, SystemTime> {
42    let now = SystemTime::now();
43
44    let mut thresholds = HashMap::with_capacity(ages_in_days.len());
45
46    for age in ages_in_days {
47        let duration = Duration::from_secs(60 * 60 * 24 * age);
48        let threshold = now - duration;
49
50        thresholds.insert(*age, threshold);
51    }
52
53    thresholds
54}
55
56fn walk(
57    dir: &Path,
58    thresholds: &HashMap<u64, SystemTime>,
59    ages_in_days: &[u64],
60    dev: Option<u64>,
61) -> Result<Data> {
62    let data = Data::default().with_ages(ages_in_days);
63
64    match fs::read_dir(dir) {
65        Ok(entries) => iterate(entries, data, thresholds, ages_in_days, dev),
66
67        Err(error) if error.kind() == ErrorKind::PermissionDenied => {
68            log::info!("skipping permission denied: {dir:?}");
69            Ok(data)
70        }
71
72        Err(error) => Err(error.into()),
73    }
74}
75
76fn iterate(
77    entries: ReadDir,
78    mut data: Data,
79    thresholds: &HashMap<u64, SystemTime>,
80    ages_in_days: &[u64],
81    dev: Option<u64>,
82) -> Result<Data> {
83    for entry in entries {
84        let entry = entry?;
85        let path = entry.path();
86        let meta = entry.metadata()?;
87        let file_type = meta.file_type();
88
89        if dev_check(dev, &meta) {
90            log::debug!("skipping different file system: {path:?}");
91        } else if file_type.is_file() {
92            log::debug!("visiting: {path:?}");
93
94            let bytes = meta.len();
95
96            let mut current =
97                Data::default().with_total_bytes(bytes).with_total_files(1);
98
99            for (age, threshold) in thresholds {
100                let (a_b, a_f) = if meta.accessed()? > *threshold {
101                    (bytes, 1)
102                } else {
103                    (0, 0)
104                };
105
106                let (m_b, m_f) = if meta.modified()? > *threshold {
107                    (bytes, 1)
108                } else {
109                    (0, 0)
110                };
111
112                current.insert(*age, a_b, m_b, a_f, m_f);
113            }
114
115            data += current;
116        } else if file_type.is_dir() {
117            log::debug!("descending: {path:?}");
118
119            data += walk(&path, thresholds, ages_in_days, dev)?;
120        } else {
121            log::debug!(
122                "skipping neither regular file nor directory: {path:?}"
123            );
124        }
125    }
126
127    Ok(data)
128}
129
130#[cfg(target_family = "unix")]
131fn dev_check(dev: Option<u64>, meta: &fs::Metadata) -> bool {
132    dev.map_or(false, |dev| dev != meta.dev())
133}
134
135#[cfg(not(target_family = "unix"))]
136const fn dev_check(_dev: Option<u64>, _meta: &fs::Metadata) -> bool {
137    false
138}