agtrace_runtime/ops/
index.rs

1use 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}