1use std::path::PathBuf;
2
3use crate::sink::HyphaError;
4
5use super::{
6 dir_size, read_spore_metadata, CachedSpore, DomainCache, HyphaConfig, TasteVerdictCache,
7};
8
9const CACHE_SENTINEL: &str = ".cmn-cache";
14
15pub struct CacheDir {
17 pub root: PathBuf,
18 pub cmn_ttl_ms: u64,
19 pub spore_max_download_bytes: u64,
20 pub spore_max_extract_bytes: u64,
21 pub spore_max_extract_files: u64,
22 pub spore_max_extract_file_bytes: u64,
23 pub spore_reject_path_components: Vec<String>,
24}
25
26impl CacheDir {
27 pub fn new() -> Result<Self, crate::sink::HyphaError> {
29 let cfg = HyphaConfig::load()?;
30 Self::from_config(&cfg)
31 }
32
33 fn from_config(cfg: &crate::config::HyphaConfig) -> Result<Self, crate::sink::HyphaError> {
34 let root = match &cfg.cache.path {
35 Some(p) => PathBuf::from(p),
36 None => crate::config::hypha_dir().join("cache"),
37 };
38
39 if !root.exists() {
40 std::fs::create_dir_all(&root).map_err(|e| {
41 HyphaError::new(
42 "cache_dir_error",
43 format!("Failed to create cache directory {}: {}", root.display(), e),
44 )
45 })?;
46 #[cfg(unix)]
47 {
48 use std::os::unix::fs::PermissionsExt;
49 std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700)).map_err(
50 |e| {
51 HyphaError::new(
52 "cache_dir_error",
53 format!(
54 "Failed to protect cache directory {}: {}",
55 root.display(),
56 e
57 ),
58 )
59 },
60 )?;
61 }
62 let _ = std::fs::write(root.join(CACHE_SENTINEL), b"cmn hypha cache\n");
66 }
67
68 Ok(Self {
69 root,
70 cmn_ttl_ms: cfg.cache.cmn_ttl_s * 1000,
71 spore_max_download_bytes: cfg.cache.spore_max_download_bytes,
72 spore_max_extract_bytes: cfg.cache.spore_max_extract_bytes,
73 spore_max_extract_files: cfg.cache.spore_max_extract_files,
74 spore_max_extract_file_bytes: cfg.cache.spore_max_extract_file_bytes,
75 spore_reject_path_components: cfg.cache.spore_reject_path_components.clone(),
76 })
77 }
78
79 pub fn domain(&self, domain: &str) -> DomainCache {
81 DomainCache {
82 root: self.root.join(domain),
83 domain: domain.to_string(),
84 }
85 }
86
87 pub fn spore_path(&self, domain: &str, hash: &str) -> PathBuf {
89 self.domain(domain).spore_path(hash)
90 }
91
92 pub fn list_all(&self) -> Vec<CachedSpore> {
94 let mut spores = Vec::new();
95
96 if !self.root.exists() {
97 return spores;
98 }
99
100 if let Ok(domains) = std::fs::read_dir(&self.root) {
101 for domain_entry in domains.filter_map(|e| e.ok()) {
102 let domain_path = domain_entry.path();
103 if !domain_path.is_dir() {
104 continue;
105 }
106
107 let domain = domain_entry.file_name().to_string_lossy().to_string();
108 let domain_cache = self.domain(&domain);
109
110 let spore_dir = domain_cache.spore_dir();
111 if let Ok(hashes) = std::fs::read_dir(&spore_dir) {
112 for hash_entry in hashes.filter_map(|e| e.ok()) {
113 let hash_path = hash_entry.path();
114 if !hash_path.is_dir() {
115 continue;
116 }
117
118 let hash_dir = hash_entry.file_name().to_string_lossy().to_string();
119 let hash = hash_dir.replace('_', ":");
120 let manifest_path = hash_path.join("spore.json");
121 let (name, synopsis) = read_spore_metadata(&manifest_path);
122
123 let verdict = {
124 let taste_path = hash_path.join("taste.json");
125 if taste_path.exists() {
126 std::fs::read_to_string(&taste_path)
127 .ok()
128 .and_then(|s| {
129 serde_json::from_str::<TasteVerdictCache>(&s).ok()
130 })
131 .map(|v| v.verdict)
132 } else {
133 None
134 }
135 };
136
137 let size = dir_size(&hash_path);
138
139 spores.push(CachedSpore {
140 domain: domain.clone(),
141 hash,
142 name,
143 synopsis,
144 path: hash_path,
145 size,
146 verdict,
147 });
148 }
149 }
150 }
151 }
152
153 spores
154 }
155
156 pub fn clean_all(&self) -> Result<usize, crate::sink::HyphaError> {
162 if !self.root.exists() {
163 return Ok(0);
164 }
165
166 let is_default = self.root == crate::config::hypha_dir().join("cache");
167 let sentinel = self.root.join(CACHE_SENTINEL);
168 if !is_default && !sentinel.exists() {
169 return Err(HyphaError::with_hint(
170 "cache_clean_refused",
171 format!(
172 "Refusing to clean {}: not a recognized hypha cache (missing {} marker)",
173 self.root.display(),
174 CACHE_SENTINEL
175 ),
176 "if this really is your cache directory, remove its contents manually",
177 ));
178 }
179
180 let count = self.list_all().len();
181
182 if let Ok(entries) = std::fs::read_dir(&self.root) {
185 for entry in entries.filter_map(|e| e.ok()) {
186 let path = entry.path();
187 if !path.is_dir() {
188 continue;
189 }
190 std::fs::remove_dir_all(&path).map_err(|e| {
191 HyphaError::new(
192 "cache_clean_failed",
193 format!("Failed to remove {}: {}", path.display(), e),
194 )
195 })?;
196 }
197 }
198
199 Ok(count)
200 }
201}
202
203impl CacheDir {
204 #[cfg(test)]
206 pub fn with_root(root: PathBuf) -> Self {
207 Self {
208 root,
209 cmn_ttl_ms: 300 * 1000,
210 spore_max_download_bytes: 1024 * 1024 * 1024,
211 spore_max_extract_bytes: 512 * 1024 * 1024,
212 spore_max_extract_files: 100_000,
213 spore_max_extract_file_bytes: 256 * 1024 * 1024,
214 spore_reject_path_components: vec![".git".to_string(), ".cmn".to_string()],
215 }
216 }
217}