agtrace_runtime/ops/
index.rs1use agtrace_index::{Database, LogFileRecord, ProjectRecord, SessionRecord};
2use agtrace_providers::{ProviderAdapter, ScanContext};
3use anyhow::{Context, Result};
4use std::collections::HashSet;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone)]
8pub enum IndexProgress {
9 IncrementalHint {
10 indexed_files: usize,
11 },
12 LogRootMissing {
13 provider_name: String,
14 log_root: PathBuf,
15 },
16 ProviderScanning {
17 provider_name: String,
18 },
19 ProviderSessionCount {
20 provider_name: String,
21 count: usize,
22 project_hash: String,
23 all_projects: bool,
24 },
25 SessionRegistered {
26 session_id: String,
27 },
28 Completed {
29 total_sessions: usize,
30 scanned_files: usize,
31 skipped_files: usize,
32 },
33}
34
35pub struct IndexService<'a> {
36 db: &'a Database,
37 providers: Vec<(ProviderAdapter, PathBuf)>,
38}
39
40impl<'a> IndexService<'a> {
41 pub fn new(db: &'a Database, providers: Vec<(ProviderAdapter, PathBuf)>) -> Self {
42 Self { db, providers }
43 }
44
45 pub fn run<F>(&self, scan_context: &ScanContext, force: bool, mut on_progress: F) -> Result<()>
46 where
47 F: FnMut(IndexProgress),
48 {
49 let indexed_files = if force {
50 HashSet::new()
51 } else {
52 self.db
53 .get_all_log_files()?
54 .into_iter()
55 .filter_map(|f| {
56 if should_skip_indexed_file(&f) {
57 Some(f.path)
58 } else {
59 None
60 }
61 })
62 .collect::<HashSet<_>>()
63 };
64
65 if !force {
66 on_progress(IndexProgress::IncrementalHint {
67 indexed_files: indexed_files.len(),
68 });
69 }
70
71 let mut total_sessions = 0;
72 let mut scanned_files = 0;
73 let mut skipped_files = 0;
74
75 for (provider, log_root) in &self.providers {
76 let provider_name = provider.id();
77
78 if !log_root.exists() {
79 on_progress(IndexProgress::LogRootMissing {
80 provider_name: provider_name.to_string(),
81 log_root: log_root.clone(),
82 });
83 continue;
84 }
85
86 on_progress(IndexProgress::ProviderScanning {
87 provider_name: provider_name.to_string(),
88 });
89
90 let sessions = provider
91 .scan_legacy(log_root, scan_context)
92 .with_context(|| format!("Failed to scan {}", provider_name))?;
93
94 on_progress(IndexProgress::ProviderSessionCount {
95 provider_name: provider_name.to_string(),
96 count: sessions.len(),
97 project_hash: scan_context.project_hash.clone(),
98 all_projects: scan_context.project_root.is_none(),
99 });
100
101 for session in sessions {
102 let all_files_unchanged = !force
103 && session
104 .log_files
105 .iter()
106 .all(|f| indexed_files.contains(&f.path));
107
108 if all_files_unchanged {
109 skipped_files += session.log_files.len();
110 continue;
111 }
112
113 on_progress(IndexProgress::SessionRegistered {
114 session_id: session.session_id.clone(),
115 });
116
117 let project_record = ProjectRecord {
118 hash: session.project_hash.clone(),
119 root_path: session.project_root.clone(),
120 last_scanned_at: Some(chrono::Utc::now().to_rfc3339()),
121 };
122 self.db.insert_or_update_project(&project_record)?;
123
124 let session_record = SessionRecord {
125 id: session.session_id.clone(),
126 project_hash: session.project_hash.clone(),
127 provider: session.provider.clone(),
128 start_ts: session.start_ts.clone(),
129 end_ts: session.end_ts.clone(),
130 snippet: session.snippet.clone(),
131 is_valid: true,
132 };
133 self.db.insert_or_update_session(&session_record)?;
134
135 for log_file in session.log_files {
136 scanned_files += 1;
137 let log_file_record = LogFileRecord {
138 path: log_file.path,
139 session_id: session.session_id.clone(),
140 role: log_file.role,
141 file_size: log_file.file_size,
142 mod_time: log_file.mod_time,
143 };
144 self.db.insert_or_update_log_file(&log_file_record)?;
145 }
146
147 total_sessions += 1;
148 }
149 }
150
151 on_progress(IndexProgress::Completed {
152 total_sessions,
153 scanned_files,
154 skipped_files,
155 });
156
157 Ok(())
158 }
159}
160
161fn should_skip_indexed_file(indexed: &LogFileRecord) -> bool {
162 use std::path::Path;
163
164 let path = Path::new(&indexed.path);
165
166 if !path.exists() {
167 return false;
168 }
169
170 let metadata = match std::fs::metadata(path) {
171 Ok(m) => m,
172 Err(_) => return false,
173 };
174
175 if let Some(db_size) = indexed.file_size {
176 if db_size != metadata.len() as i64 {
177 return false;
178 }
179 } else {
180 return false;
181 }
182
183 if let Some(db_mod_time) = &indexed.mod_time {
184 if let Ok(fs_mod_time) = metadata.modified() {
185 let fs_mod_time_str = format!("{:?}", fs_mod_time);
186 if db_mod_time != &fs_mod_time_str {
187 return false;
188 }
189 } else {
190 return false;
191 }
192 } else {
193 return false;
194 }
195
196 true
197}