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}
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}