Mountain/ApplicationState/Internal/ExtensionScanner/
LoadFromCache.rs1use std::{collections::HashMap, path::PathBuf, time::Duration};
34
35use CommonLibrary::Error::CommonError::CommonError;
36use serde::Deserialize;
37use serde_json::Value;
38
39use crate::{ApplicationState::DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO, dev_log};
40
41#[derive(Debug, Deserialize)]
43struct CachedEntry {
44 id:String,
45 path:String,
46 manifest:Value,
47}
48
49#[derive(Debug, Deserialize)]
51struct CacheBlob {
52 version:u32,
53 #[allow(dead_code)]
54 count:u32,
55 extensions:Vec<CachedEntry>,
56}
57
58const MAX_CACHE_AGE:Duration = Duration::from_secs(600); pub async fn Fn(BinaryDir:&PathBuf) -> Result<Option<HashMap<String, ExtensionDescriptionStateDTO>>, CommonError> {
74 let DevCachePath = BinaryDir.join("extensions.manifest.json");
76 let BundleCachePath = BinaryDir.join("../Resources/extensions.manifest.json");
78
79 let (CachePath, IsBundled) = if tokio::fs::metadata(&DevCachePath).await.is_ok() {
81 (DevCachePath, false)
82 } else if tokio::fs::metadata(&BundleCachePath).await.is_ok() {
83 (BundleCachePath, true)
84 } else {
85 dev_log!("extensions", "[ExtensionCache] Cache not found at {}", DevCachePath.display());
86 return Ok(None);
87 };
88
89 let Age = if IsBundled {
91 Duration::ZERO
92 } else {
93 let Metadata = tokio::fs::metadata(&CachePath)
94 .await
95 .map_err(|_| CommonError::Unknown { Description:"cache stat failed".into() })?;
96 Metadata.modified().ok().and_then(|T| T.elapsed().ok()).unwrap_or(Duration::MAX)
97 };
98 if !IsBundled && Age > MAX_CACHE_AGE {
99 dev_log!(
100 "extensions",
101 "[ExtensionCache] Cache is stale ({:.0}s > {:.0}s), falling back to live scan",
102 Age.as_secs_f32(),
103 MAX_CACHE_AGE.as_secs_f32()
104 );
105 return Ok(None);
106 }
107
108 let Bytes = match tokio::fs::read(&CachePath).await {
110 Ok(B) => B,
111 Err(E) => {
112 dev_log!(
113 "extensions",
114 "warn: [ExtensionCache] Read failed: {}; falling back to live scan",
115 E
116 );
117 return Ok(None);
118 },
119 };
120
121 let Blob:CacheBlob = match serde_json::from_slice(&Bytes) {
122 Ok(B) => B,
123 Err(E) => {
124 dev_log!(
125 "extensions",
126 "warn: [ExtensionCache] Parse error: {}; falling back to live scan",
127 E
128 );
129 return Ok(None);
130 },
131 };
132
133 if Blob.version != 1 {
134 dev_log!(
135 "extensions",
136 "[ExtensionCache] Unsupported cache version {}; falling back to live scan",
137 Blob.version
138 );
139 return Ok(None);
140 }
141
142 let mut Map:HashMap<String, ExtensionDescriptionStateDTO> = HashMap::with_capacity(Blob.extensions.len());
144
145 for Entry in Blob.extensions {
146 let Manifest = &Entry.manifest;
147 let Path = &Entry.path;
148
149 let str = |k:&str| Manifest.get(k).and_then(Value::as_str).map(str::to_string);
151 let str_or = |k:&str, d:&str| Manifest.get(k).and_then(Value::as_str).unwrap_or(d).to_string();
152 let arr =
153 |k:&str| -> Option<Vec<String>> { Manifest.get(k).and_then(|V| serde_json::from_value(V.clone()).ok()) };
154
155 let ExtId = Entry.id.clone();
156 let Publisher = Manifest
157 .get("publisher")
158 .and_then(Value::as_str)
159 .unwrap_or_else(|| Entry.id.split('.').next().unwrap_or("unknown"))
160 .to_string();
161
162 let IsBuiltin = PathBuf::from(Path)
164 .parent()
165 .and_then(|P| P.file_name())
166 .and_then(|N| N.to_str())
167 .map(|N| N == "extensions")
168 .unwrap_or(false);
169
170 let Dto = ExtensionDescriptionStateDTO {
171 Identifier:serde_json::json!({ "value": ExtId }),
172 Name:str_or("name", ""),
173 Version:str_or("version", "0.0.0"),
174 Publisher,
175 Engines:Manifest.get("engines").cloned().unwrap_or(serde_json::json!({})),
176 Main:str("main"),
177 Browser:str("browser"),
178 ModuleType:str("type"),
179 IsBuiltin,
180 IsUnderDevelopment:false,
181 ExtensionLocation:Value::String(format!("file://{}", Path)),
184 ActivationEvents:arr("activationEvents"),
185 Contributes:Manifest.get("contributes").cloned(),
186 Categories:arr("categories"),
187 DisplayName:str("displayName"),
188 Description:str("description"),
189 Keywords:arr("keywords"),
190 Repository:Manifest.get("repository").cloned(),
191 Bugs:Manifest.get("bugs").cloned(),
192 Homepage:str("homepage"),
193 License:str("license"),
194 Icon:str("icon"),
195 AiKey:str("aiKey"),
196 ExtensionKind:Manifest.get("extensionKind").cloned(),
197 Capabilities:Manifest.get("capabilities").cloned(),
198 ExtensionDependencies:arr("extensionDependencies"),
199 ExtensionPack:arr("extensionPack"),
200 };
201
202 Map.insert(ExtId, Dto);
203 }
204
205 dev_log!(
206 "extensions",
207 "[ExtensionCache] Loaded {} extensions from {} cache ({} bytes{})",
208 Map.len(),
209 if IsBundled { "bundled" } else { "dev" },
210 Bytes.len(),
211 if IsBundled {
212 String::new()
213 } else {
214 format!(", {:.0}s old", Age.as_secs_f32())
215 }
216 );
217
218 Ok(Some(Map))
219}