Skip to main content

tandem_runtime/
workspace_index.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use ignore::WalkBuilder;
5use serde::Serialize;
6use tokio::sync::RwLock;
7
8#[derive(Debug, Clone, Serialize, Default)]
9pub struct WorkspaceIndexSnapshot {
10    pub root: String,
11    pub file_count: usize,
12    pub indexed_at: Option<String>,
13    pub largest_files: Vec<IndexedFile>,
14}
15
16#[derive(Debug, Clone, Serialize)]
17pub struct IndexedFile {
18    pub path: String,
19    pub bytes: u64,
20}
21
22#[derive(Clone)]
23pub struct WorkspaceIndex {
24    root: Arc<PathBuf>,
25    snapshot: Arc<RwLock<WorkspaceIndexSnapshot>>,
26}
27
28impl WorkspaceIndex {
29    pub async fn new(root: impl Into<PathBuf>) -> Self {
30        let root = root.into();
31        let initial = WorkspaceIndexSnapshot {
32            root: root.to_string_lossy().to_string(),
33            ..WorkspaceIndexSnapshot::default()
34        };
35        let this = Self {
36            root: Arc::new(root),
37            snapshot: Arc::new(RwLock::new(initial)),
38        };
39        let clone = this.clone();
40        tokio::spawn(async move {
41            let _ = clone.refresh().await;
42        });
43        this
44    }
45
46    pub async fn refresh(&self) -> WorkspaceIndexSnapshot {
47        let root = self.root.clone();
48        let (mut files, count) = tokio::task::spawn_blocking(move || {
49            let mut files = Vec::new();
50            let mut count = 0usize;
51            for entry in WalkBuilder::new(root.as_path()).build().flatten() {
52                if !entry.file_type().map(|f| f.is_file()).unwrap_or(false) {
53                    continue;
54                }
55                count += 1;
56                if let Ok(meta) = entry.metadata() {
57                    files.push(IndexedFile {
58                        path: relativize(root.as_path(), entry.path()),
59                        bytes: meta.len(),
60                    });
61                }
62            }
63            (files, count)
64        })
65        .await
66        .unwrap_or_default();
67
68        files.sort_by(|a, b| b.bytes.cmp(&a.bytes));
69        let largest_files = files.into_iter().take(20).collect::<Vec<_>>();
70        let snapshot = WorkspaceIndexSnapshot {
71            root: self.root.to_string_lossy().to_string(),
72            file_count: count,
73            indexed_at: Some(chrono::Utc::now().to_rfc3339()),
74            largest_files,
75        };
76        *self.snapshot.write().await = snapshot.clone();
77        snapshot
78    }
79
80    pub async fn snapshot(&self) -> WorkspaceIndexSnapshot {
81        self.snapshot.read().await.clone()
82    }
83}
84
85fn relativize(root: &std::path::Path, path: &std::path::Path) -> String {
86    path.strip_prefix(root)
87        .map(|v| v.to_string_lossy().to_string())
88        .unwrap_or_else(|_| path.to_string_lossy().to_string())
89}