AirLibrary/Indexing/Store/
StoreEntry.rs1use std::path::{Path, PathBuf};
66
67use crate::{AirError, Indexing::State::CreateState::FileIndex, Result, dev_log};
68
69pub async fn SaveIndex(index_directory:&Path, index:&FileIndex) -> Result<()> {
71 let index_file = index_directory.join("file_index.json");
72 let temp_file = index_directory.join("file_index.json.tmp");
73
74 let content = serde_json::to_string_pretty(index)
75 .map_err(|e| AirError::Serialization(format!("Failed to serialize index: {}", e)))?;
76
77 tokio::fs::write(&temp_file, content)
79 .await
80 .map_err(|e| AirError::FileSystem(format!("Failed to write temp index file: {}", e)))?;
81
82 tokio::fs::rename(&temp_file, &index_file)
84 .await
85 .map_err(|e| AirError::FileSystem(format!("Failed to rename index file: {}", e)))?;
86
87 dev_log!(
88 "indexing",
89 "[StoreEntry] Index saved to: {} ({} files, {} symbols)",
90 index_file.display(),
91 index.files.len(),
92 index.symbol_index.len()
93 );
94
95 Ok(())
96}
97
98pub async fn LoadIndex(index_directory:&Path) -> Result<FileIndex> {
100 let index_file = index_directory.join("file_index.json");
101
102 if !index_file.exists() {
103 return Err(AirError::FileSystem(format!(
104 "Index file does not exist: {}",
105 index_file.display()
106 )));
107 }
108
109 let content = tokio::fs::read_to_string(&index_file)
110 .await
111 .map_err(|e| AirError::FileSystem(format!("Failed to read index file: {}", e)))?;
112
113 let index:FileIndex = serde_json::from_str(&content)
114 .map_err(|e| AirError::Serialization(format!("Failed to parse index file: {}", e)))?;
115
116 if index.index_version.is_empty() || index.index_checksum.is_empty() {
118 return Err(AirError::Serialization("Index missing version or checksum".to_string()));
119 }
120
121 use crate::Indexing::State::CreateState::CalculateIndexChecksum;
123 let expected_checksum = CalculateIndexChecksum(&index)?;
124 if index.index_checksum != expected_checksum {
125 return Err(AirError::Serialization(format!(
126 "Index checksum mismatch: expected {}, got {}",
127 expected_checksum, index.index_checksum
128 )));
129 }
130
131 Ok(index)
132}
133
134pub async fn LoadOrCreateIndex(index_directory:&Path) -> Result<FileIndex> {
136 let index_file = index_directory.join("file_index.json");
137
138 if index_file.exists() {
139 match LoadIndex(index_directory).await {
141 Ok(index) => {
142 dev_log!("indexing", "[StoreEntry] Loaded index with {} files", index.files.len());
143 Ok(index)
144 },
145 Err(e) => {
146 dev_log!(
147 "indexing",
148 "warn: [StoreEntry] Failed to load index (may be corrupted): {}. Creating new index.",
149 e
150 );
151 BackupCorruptedIndex(index_directory).await?;
153 Ok(CreateNewIndex())
154 },
155 }
156 } else {
157 Ok(CreateNewIndex())
159 }
160}
161
162fn CreateNewIndex() -> FileIndex {
164 use crate::Indexing::State::CreateState::CreateNewIndex as StateCreateNewIndex;
165 StateCreateNewIndex()
166}
167
168pub async fn EnsureIndexDirectory(index_directory:&Path) -> Result<()> {
170 tokio::fs::create_dir_all(index_directory).await.map_err(|e| {
171 AirError::Configuration(format!("Failed to create index directory {}: {}", index_directory.display(), e))
172 })?;
173 Ok(())
174}
175
176pub async fn BackupCorruptedIndex(index_directory:&Path) -> Result<()> {
178 let index_file = index_directory.join("file_index.json");
179 let backup_file = index_directory.join(format!("file_index.corrupted.{}.json", chrono::Utc::now().timestamp()));
180
181 if !index_file.exists() {
182 return Ok(());
183 }
184
185 tokio::fs::rename(&index_file, &backup_file)
187 .await
188 .map_err(|e| AirError::FileSystem(format!("Failed to backup corrupted index: {}", e)))?;
189
190 dev_log!(
191 "indexing",
192 "[StoreEntry] Backed up corrupted index to: {}",
193 backup_file.display()
194 );
195 Ok(())
196}
197
198pub async fn LoadIndexWithRecovery(index_directory:&Path, max_retries:usize) -> Result<FileIndex> {
200 let mut last_error = None;
201
202 for attempt in 0..max_retries {
203 match LoadOrCreateIndex(index_directory).await {
204 Ok(index) => {
205 if attempt > 0 {
206 dev_log!(
207 "indexing",
208 "[StoreEntry] Successfully loaded index after {} attempts",
209 attempt + 1
210 );
211 }
212 return Ok(index);
213 },
214 Err(e) => {
215 last_error = Some(e);
216 dev_log!("indexing", "warn: [StoreEntry] Load attempt {} failed", attempt + 1);
217 if attempt < max_retries - 1 {
219 tokio::time::sleep(tokio::time::Duration::from_millis(100 * (attempt + 1) as u64)).await;
220 }
221 },
222 }
223 }
224
225 Err(last_error.unwrap_or_else(|| AirError::Internal("Failed to load index after retries".to_string())))
226}
227
228pub fn GetIndexFilePath(index_directory:&Path) -> PathBuf { index_directory.join("file_index.json") }
230
231pub async fn IndexFileExists(index_directory:&Path) -> Result<bool> {
233 let index_file = index_directory.join("file_index.json");
234
235 if !index_file.exists() {
236 return Ok(false);
237 }
238
239 match tokio::fs::metadata(&index_file).await {
241 Ok(_) => Ok(true),
242 Err(_) => Ok(false),
243 }
244}
245
246pub async fn GetIndexFileSize(index_directory:&Path) -> Result<u64> {
248 let index_file = index_directory.join("file_index.json");
249
250 let metadata = tokio::fs::metadata(&index_file)
251 .await
252 .map_err(|e| AirError::FileSystem(format!("Failed to get index file metadata: {}", e)))?;
253
254 Ok(metadata.len())
255}
256
257pub async fn CleanupOldBackups(index_directory:&Path, keep_count:usize) -> Result<usize> {
259 let mut entries = tokio::fs::read_dir(index_directory)
260 .await
261 .map_err(|e| AirError::FileSystem(format!("Failed to read index directory: {}", e)))?;
262
263 let mut backups = Vec::new();
264
265 while let Some(entry) = entries
266 .next_entry()
267 .await
268 .map_err(|e| AirError::FileSystem(format!("Failed to read directory entry: {}", e)))?
269 {
270 let file_name = entry.file_name().to_string_lossy().to_string();
271
272 if file_name.starts_with("file_index.corrupted.") && file_name.ends_with(".json") {
273 if let Ok(metadata) = entry.metadata().await {
274 if let Ok(modified) = metadata.modified() {
275 backups.push((entry.path(), modified));
276 }
277 }
278 }
279 }
280
281 backups.sort_by_key(|b| b.1);
283
284 let mut removed_count = 0;
285
286 for (path, _) in backups.iter().take(backups.len().saturating_sub(keep_count)) {
288 match tokio::fs::remove_file(path).await {
289 Ok(_) => {
290 dev_log!("indexing", "[StoreEntry] Removed old backup: {}", path.display());
291 removed_count += 1;
292 },
293 Err(e) => {
294 dev_log!(
295 "indexing",
296 "warn: [StoreEntry] Failed to remove backup {}: {}",
297 path.display(),
298 e
299 );
300 },
301 }
302 }
303
304 Ok(removed_count)
305}
306
307pub async fn ValidateIndexFormat(index_directory:&Path) -> Result<()> {
309 let index_file = index_directory.join("file_index.json");
310
311 let content = tokio::fs::read_to_string(&index_file)
312 .await
313 .map_err(|e| AirError::FileSystem(format!("Failed to read index file: {}", e)))?;
314
315 let _:serde_json::Value = serde_json::from_str(&content)
317 .map_err(|e| AirError::Serialization(format!("Index file is not valid JSON: {}", e)))?;
318
319 Ok(())
320}