Skip to main content

provenant/cache/
config.rs

1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use directories::ProjectDirs;
6
7use super::locking::scans_lock_path;
8
9pub const DEFAULT_CACHE_DIR_NAME: &str = ".provenant-cache";
10pub const CACHE_DIR_ENV_VAR: &str = "PROVENANT_CACHE";
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct CacheConfig {
14    root_dir: PathBuf,
15    incremental: bool,
16}
17
18impl CacheConfig {
19    #[cfg(test)]
20    pub fn new(root_dir: PathBuf) -> Self {
21        Self {
22            root_dir,
23            incremental: false,
24        }
25    }
26
27    pub fn with_options(root_dir: PathBuf, incremental: bool) -> Self {
28        Self {
29            root_dir,
30            incremental,
31        }
32    }
33
34    #[cfg(test)]
35    pub fn from_scan_root(scan_root: &Path) -> Self {
36        Self::new(scan_root.join(DEFAULT_CACHE_DIR_NAME))
37    }
38
39    fn project_cache_root() -> Option<PathBuf> {
40        ProjectDirs::from("com", "Provenant", "provenant")
41            .map(|dirs| dirs.cache_dir().to_path_buf())
42    }
43
44    pub fn default_root_dir(scan_root: &Path) -> PathBuf {
45        Self::project_cache_root().unwrap_or_else(|| scan_root.join(DEFAULT_CACHE_DIR_NAME))
46    }
47
48    pub fn resolve_root_dir(
49        scan_root: &Path,
50        cli_cache_dir: Option<&Path>,
51        env_cache_dir: Option<&Path>,
52    ) -> PathBuf {
53        if let Some(path) = cli_cache_dir {
54            return path.to_path_buf();
55        }
56
57        if let Some(path) = env_cache_dir {
58            return path.to_path_buf();
59        }
60
61        Self::default_root_dir(scan_root)
62    }
63
64    pub fn from_overrides(
65        scan_root: &Path,
66        cli_cache_dir: Option<&Path>,
67        env_cache_dir: Option<&Path>,
68        incremental: bool,
69    ) -> Self {
70        Self::with_options(
71            Self::resolve_root_dir(scan_root, cli_cache_dir, env_cache_dir),
72            incremental,
73        )
74    }
75
76    pub fn root_dir(&self) -> &Path {
77        &self.root_dir
78    }
79
80    pub fn incremental_dir(&self) -> PathBuf {
81        self.root_dir.join("incremental")
82    }
83
84    pub const fn incremental_enabled(&self) -> bool {
85        self.incremental
86    }
87
88    pub fn ensure_dirs(&self) -> io::Result<()> {
89        if self.incremental_enabled() {
90            fs::create_dir_all(self.incremental_dir())?;
91        }
92        Ok(())
93    }
94
95    #[cfg(test)]
96    pub fn clear(&self) -> io::Result<()> {
97        if self.root_dir().exists() {
98            fs::remove_dir_all(&self.root_dir)?;
99        }
100        Ok(())
101    }
102
103    pub fn clear_contents(&self) -> io::Result<()> {
104        if !self.root_dir().exists() {
105            return Ok(());
106        }
107
108        let lock_path = scans_lock_path(self.root_dir());
109        for entry in fs::read_dir(self.root_dir())? {
110            let entry = entry?;
111            let path = entry.path();
112            if path == lock_path {
113                continue;
114            }
115
116            if path.is_dir() {
117                fs::remove_dir_all(path)?;
118            } else {
119                fs::remove_file(path)?;
120            }
121        }
122
123        Ok(())
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use tempfile::TempDir;
130
131    use super::*;
132
133    #[test]
134    fn test_from_scan_root_uses_expected_directory_name() {
135        let temp_dir = TempDir::new().expect("Failed to create temp dir");
136        let config = CacheConfig::from_scan_root(temp_dir.path());
137        assert_eq!(
138            config.root_dir(),
139            temp_dir.path().join(DEFAULT_CACHE_DIR_NAME)
140        );
141    }
142
143    #[test]
144    fn test_ensure_dirs_creates_expected_tree() {
145        let temp_dir = TempDir::new().expect("Failed to create temp dir");
146        let config = CacheConfig::with_options(temp_dir.path().join(DEFAULT_CACHE_DIR_NAME), true);
147
148        config
149            .ensure_dirs()
150            .expect("Failed to create cache directories");
151
152        assert!(config.root_dir().exists());
153        assert!(config.incremental_dir().exists());
154    }
155
156    #[test]
157    fn test_resolve_root_dir_prefers_cli_then_env_then_default() {
158        let scan_root = Path::new("/scan-root");
159        let cli_dir = Path::new("/cli-cache");
160        let env_dir = Path::new("/env-cache");
161
162        assert_eq!(
163            CacheConfig::resolve_root_dir(scan_root, Some(cli_dir), Some(env_dir)),
164            cli_dir
165        );
166        assert_eq!(
167            CacheConfig::resolve_root_dir(scan_root, None, Some(env_dir)),
168            env_dir
169        );
170        assert_eq!(
171            CacheConfig::resolve_root_dir(scan_root, None, None),
172            CacheConfig::default_root_dir(scan_root)
173        );
174    }
175
176    #[test]
177    fn test_clear_removes_cache_root_directory() {
178        let temp_dir = TempDir::new().expect("Failed to create temp dir");
179        let config = CacheConfig::with_options(temp_dir.path().join("cache-root"), true);
180
181        config
182            .ensure_dirs()
183            .expect("Failed to create cache directories");
184        assert!(config.root_dir().exists());
185
186        config.clear().expect("Failed to clear cache directory");
187        assert!(!config.root_dir().exists());
188    }
189}