Skip to main content

provenant/cache/
config.rs

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