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    pub(crate) 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 default_root_dir_without_scan_root() -> PathBuf {
49        Self::project_cache_root().unwrap_or_else(|| PathBuf::from(DEFAULT_CACHE_DIR_NAME))
50    }
51
52    pub fn resolve_root_dir(
53        scan_root: Option<&Path>,
54        cli_cache_dir: Option<&Path>,
55        env_cache_dir: Option<&Path>,
56    ) -> PathBuf {
57        if let Some(path) = cli_cache_dir {
58            return path.to_path_buf();
59        }
60
61        if let Some(path) = env_cache_dir {
62            return path.to_path_buf();
63        }
64
65        match scan_root {
66            Some(scan_root) => Self::default_root_dir(scan_root),
67            None => Self::default_root_dir_without_scan_root(),
68        }
69    }
70
71    pub fn from_overrides(
72        scan_root: Option<&Path>,
73        cli_cache_dir: Option<&Path>,
74        env_cache_dir: Option<&Path>,
75        incremental: bool,
76    ) -> Self {
77        Self::with_options(
78            Self::resolve_root_dir(scan_root, cli_cache_dir, env_cache_dir),
79            incremental,
80        )
81    }
82
83    pub fn root_dir(&self) -> &Path {
84        &self.root_dir
85    }
86
87    pub fn incremental_dir(&self) -> PathBuf {
88        self.root_dir.join("incremental")
89    }
90
91    pub const fn incremental_enabled(&self) -> bool {
92        self.incremental
93    }
94
95    pub fn ensure_dirs(&self) -> io::Result<()> {
96        if self.incremental_enabled() {
97            fs::create_dir_all(self.incremental_dir())?;
98        }
99        Ok(())
100    }
101
102    #[cfg(test)]
103    pub fn clear(&self) -> io::Result<()> {
104        if self.root_dir().exists() {
105            fs::remove_dir_all(&self.root_dir)?;
106        }
107        Ok(())
108    }
109
110    pub fn clear_contents(&self) -> io::Result<()> {
111        if !self.root_dir().exists() {
112            return Ok(());
113        }
114
115        let lock_path = scans_lock_path(self.root_dir());
116        for entry in fs::read_dir(self.root_dir())? {
117            let entry = entry?;
118            let path = entry.path();
119            if path == lock_path {
120                continue;
121            }
122
123            if path.is_dir() {
124                fs::remove_dir_all(path)?;
125            } else {
126                fs::remove_file(path)?;
127            }
128        }
129
130        Ok(())
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use tempfile::TempDir;
137
138    use super::*;
139
140    #[test]
141    fn test_from_scan_root_uses_expected_directory_name() {
142        let temp_dir = TempDir::new().expect("Failed to create temp dir");
143        let config = CacheConfig::from_scan_root(temp_dir.path());
144        assert_eq!(
145            config.root_dir(),
146            temp_dir.path().join(DEFAULT_CACHE_DIR_NAME)
147        );
148    }
149
150    #[test]
151    fn test_ensure_dirs_creates_expected_tree() {
152        let temp_dir = TempDir::new().expect("Failed to create temp dir");
153        let config = CacheConfig::with_options(temp_dir.path().join(DEFAULT_CACHE_DIR_NAME), true);
154
155        config
156            .ensure_dirs()
157            .expect("Failed to create cache directories");
158
159        assert!(config.root_dir().exists());
160        assert!(config.incremental_dir().exists());
161    }
162
163    #[test]
164    fn test_resolve_root_dir_prefers_cli_then_env_then_default() {
165        let scan_root = Path::new("/scan-root");
166        let cli_dir = Path::new("/cli-cache");
167        let env_dir = Path::new("/env-cache");
168
169        assert_eq!(
170            CacheConfig::resolve_root_dir(Some(scan_root), Some(cli_dir), Some(env_dir)),
171            cli_dir
172        );
173        assert_eq!(
174            CacheConfig::resolve_root_dir(Some(scan_root), None, Some(env_dir)),
175            env_dir
176        );
177        assert_eq!(
178            CacheConfig::resolve_root_dir(Some(scan_root), None, None),
179            CacheConfig::default_root_dir(scan_root)
180        );
181    }
182
183    #[test]
184    fn test_resolve_root_dir_without_scan_root_uses_project_or_relative_default() {
185        assert_eq!(
186            CacheConfig::resolve_root_dir(None, None, None),
187            CacheConfig::default_root_dir_without_scan_root()
188        );
189    }
190
191    #[test]
192    fn test_clear_removes_cache_root_directory() {
193        let temp_dir = TempDir::new().expect("Failed to create temp dir");
194        let config = CacheConfig::with_options(temp_dir.path().join("cache-root"), true);
195
196        config
197            .ensure_dirs()
198            .expect("Failed to create cache directories");
199        assert!(config.root_dir().exists());
200
201        config.clear().expect("Failed to clear cache directory");
202        assert!(!config.root_dir().exists());
203    }
204}