qex_core/index/
storage.rs1use crate::index::IndexResult;
2use anyhow::{Context, Result};
3use sha2::{Digest, Sha256};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7pub struct ProjectStorage {
19 base_dir: PathBuf,
20}
21
22impl ProjectStorage {
23 pub fn for_project(project_path: &Path) -> Result<Self> {
25 let normalized = project_path
26 .canonicalize()
27 .unwrap_or_else(|_| project_path.to_path_buf());
28 let path_str = normalized.to_string_lossy();
29
30 let project_name = normalized
32 .file_name()
33 .and_then(|n| n.to_str())
34 .unwrap_or("unknown");
35
36 let hash = {
38 let mut hasher = Sha256::new();
39 hasher.update(path_str.as_bytes());
40 format!("{:x}", hasher.finalize())[..8].to_string()
41 };
42
43 let dir_name = format!("{}_{}", sanitize_name(project_name), hash);
44
45 let base_dir = qex_home()?.join("projects").join(dir_name);
46 fs::create_dir_all(&base_dir)
47 .context("Failed to create project storage directory")?;
48
49 let info_path = base_dir.join("project_info.json");
51 if !info_path.exists() {
52 let info = serde_json::json!({
53 "project_path": path_str.as_ref(),
54 "project_name": project_name,
55 });
56 fs::write(&info_path, serde_json::to_string_pretty(&info)?)?;
57 }
58
59 Ok(Self { base_dir })
60 }
61
62 pub fn base_dir(&self) -> &Path {
63 &self.base_dir
64 }
65
66 pub fn tantivy_dir(&self) -> PathBuf {
67 self.base_dir.join("tantivy")
68 }
69
70 pub fn dense_dir(&self) -> PathBuf {
71 self.base_dir.join("dense")
72 }
73
74 pub fn has_index(&self) -> bool {
76 self.tantivy_dir().join("meta.json").exists()
77 }
78
79 pub fn save_stats(&self, result: &IndexResult) -> Result<()> {
81 let stats_path = self.base_dir.join("stats.json");
82 let json = serde_json::to_string_pretty(result)?;
83 fs::write(stats_path, json)?;
84 Ok(())
85 }
86
87 pub fn load_stats(&self) -> Result<Option<IndexResult>> {
89 let stats_path = self.base_dir.join("stats.json");
90 if !stats_path.exists() {
91 return Ok(None);
92 }
93 let json = fs::read_to_string(stats_path)?;
94 let stats: IndexResult = serde_json::from_str(&json)?;
95 Ok(Some(stats))
96 }
97
98 pub fn clear(&self) -> Result<()> {
100 let snapshot = self.base_dir.join("snapshot.json");
101 let metadata = self.base_dir.join("snapshot_metadata.json");
102 let stats = self.base_dir.join("stats.json");
103
104 for path in [snapshot, metadata, stats] {
105 if path.exists() {
106 fs::remove_file(path)?;
107 }
108 }
109 Ok(())
110 }
111
112 pub fn clear_all(&self) -> Result<()> {
114 if self.base_dir.exists() {
115 fs::remove_dir_all(&self.base_dir)?;
116 }
117 Ok(())
118 }
119}
120
121fn qex_home() -> Result<PathBuf> {
123 let home = dirs::home_dir().context("Could not determine home directory")?;
124 Ok(home.join(".qex"))
125}
126
127fn sanitize_name(name: &str) -> String {
129 name.chars()
130 .map(|c| if c.is_alphanumeric() || c == '-' || c == '_' { c } else { '_' })
131 .collect()
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use tempfile::TempDir;
138
139 #[test]
140 fn test_project_storage_creation() {
141 let dir = TempDir::new().unwrap();
142 let storage = ProjectStorage::for_project(dir.path()).unwrap();
143 assert!(storage.base_dir().exists());
144 assert!(storage.base_dir().join("project_info.json").exists());
145 }
146
147 #[test]
148 fn test_sanitize_name() {
149 assert_eq!(sanitize_name("my-project"), "my-project");
150 assert_eq!(sanitize_name("my project!"), "my_project_");
151 assert_eq!(sanitize_name("code_context"), "code_context");
152 }
153}