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