vtcode_indexer/
storage.rs1use anyhow::Result;
9use std::fs;
10use std::io::{ErrorKind, Write};
11use std::path::Path;
12
13use crate::FileIndex;
14use crate::atomic_write;
15use crate::calculate_hash;
16
17pub trait IndexStorage: Send + Sync {
19 fn init(&self, index_dir: &Path) -> Result<()>;
21
22 fn persist(&self, index_dir: &Path, entry: &FileIndex) -> Result<()>;
24
25 fn prefers_snapshot_persistence(&self) -> bool {
31 false
32 }
33
34 fn remove(&self, _index_dir: &Path, _file_path: &Path) -> Result<()> {
38 Ok(())
39 }
40
41 fn persist_batch(&self, index_dir: &Path, entries: &[FileIndex]) -> Result<()> {
46 for entry in entries {
47 self.persist(index_dir, entry)?;
48 }
49 Ok(())
50 }
51
52 fn persist_batch_refs(&self, index_dir: &Path, entries: &[&FileIndex]) -> Result<()> {
58 let owned = entries.iter().map(|entry| (*entry).clone()).collect::<Vec<_>>();
59 self.persist_batch(index_dir, &owned)
60 }
61}
62
63#[derive(Debug, Default, Clone)]
65pub struct MarkdownIndexStorage;
66
67impl IndexStorage for MarkdownIndexStorage {
68 fn init(&self, index_dir: &Path) -> Result<()> {
69 fs::create_dir_all(index_dir)?;
70 Ok(())
71 }
72
73 fn persist(&self, index_dir: &Path, entry: &FileIndex) -> Result<()> {
74 let dir = atomic_write::canonical_storage_dir(index_dir)?;
75 let file_name = format!("{}.md", calculate_hash(&entry.path));
76 let mut buffer = Vec::new();
77 writeln!(buffer, "# File Index: {}", entry.path)?;
78 writeln!(buffer)?;
79 write_markdown_fields(&mut buffer, entry)?;
80 atomic_write::write_atomic(&dir, &file_name, &buffer)
81 }
82
83 fn prefers_snapshot_persistence(&self) -> bool {
84 true
85 }
86
87 fn remove(&self, index_dir: &Path, file_path: &Path) -> Result<()> {
88 if !index_dir.exists() {
89 return Ok(());
90 }
91 let dir = atomic_write::canonical_storage_dir(index_dir)?;
92 let file_name = format!("{}.md", calculate_hash(file_path.to_string_lossy().as_ref()));
93 match fs::remove_file(dir.join(file_name)) {
94 Ok(()) => Ok(()),
95 Err(err) if err.kind() == ErrorKind::NotFound => Ok(()),
96 Err(err) => Err(err.into()),
97 }
98 }
99
100 fn persist_batch(&self, index_dir: &Path, entries: &[FileIndex]) -> Result<()> {
101 persist_markdown_snapshot(index_dir, entries.iter())
102 }
103
104 fn persist_batch_refs(&self, index_dir: &Path, entries: &[&FileIndex]) -> Result<()> {
105 persist_markdown_snapshot(index_dir, entries.iter().copied())
106 }
107}
108
109fn persist_markdown_snapshot<'a>(index_dir: &Path, entries: impl IntoIterator<Item = &'a FileIndex>) -> Result<()> {
110 let entries = entries.into_iter().collect::<Vec<_>>();
111
112 let dir = atomic_write::canonical_storage_dir(index_dir)?;
113 let mut buffer = Vec::new();
114
115 writeln!(buffer, "# Workspace File Index")?;
116 writeln!(buffer)?;
117 writeln!(buffer, "- **Entries**: {}", entries.len())?;
118 writeln!(buffer)?;
119
120 for entry in entries {
121 write_markdown_entry(&mut buffer, entry)?;
122 }
123
124 atomic_write::write_atomic(&dir, "index.md", &buffer)?;
125 cleanup_legacy_markdown_entries(&dir)?;
126 Ok(())
127}
128
129fn write_markdown_entry(writer: &mut impl Write, entry: &FileIndex) -> std::io::Result<()> {
130 writeln!(writer, "## {}", entry.path)?;
131 writeln!(writer)?;
132 write_markdown_fields(writer, entry)?;
133 writeln!(writer)?;
134 Ok(())
135}
136
137fn write_markdown_fields(writer: &mut impl Write, entry: &FileIndex) -> std::io::Result<()> {
138 writeln!(writer, "- **Path**: {}", entry.path)?;
139 writeln!(writer, "- **Hash**: {}", entry.hash)?;
140 writeln!(writer, "- **Modified**: {}", entry.modified)?;
141 writeln!(writer, "- **Size**: {} bytes", entry.size)?;
142 writeln!(writer, "- **Language**: {}", entry.language)?;
143 writeln!(writer, "- **Tags**: {}", entry.tags.join(", "))?;
144 Ok(())
145}
146
147fn cleanup_legacy_markdown_entries(index_dir: &Path) -> Result<()> {
148 for entry in fs::read_dir(index_dir)? {
149 let entry = entry?;
150 let file_name = entry.file_name();
151 let file_name = file_name.to_string_lossy();
152 if is_legacy_markdown_entry_name(file_name.as_ref()) {
153 fs::remove_file(entry.path())?;
154 }
155 }
156 Ok(())
157}
158
159#[inline]
160fn is_legacy_markdown_entry_name(file_name: &str) -> bool {
161 let Some(hash_part) = file_name.strip_suffix(".md") else {
162 return false;
163 };
164 hash_part.len() == 64 && hash_part.bytes().all(|byte| byte.is_ascii_hexdigit())
165}