provenant/cache/
config.rs1use 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}