Real-time index of opencode sessions
1use crate::id::{MessageId, PartId, SessionId};
2use crate::storage::mmap::{MappedFile, MappedFileCache};
3use crate::storage::paths::StoragePaths;
4use crate::types::message::FileDiff;
5use crate::types::{Message, Part, SessionInfo};
6use crate::{Error, Result};
7use serde::de::DeserializeOwned;
8use std::path::Path;
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
12pub struct MappedSpan {
13 pub file: Arc<MappedFile>,
14 pub offset: usize,
15 pub len: usize,
16}
17
18impl MappedSpan {
19 pub fn as_bytes(&self) -> &[u8] {
20 &self.file.as_bytes()[self.offset..self.offset + self.len]
21 }
22}
23
24pub struct FileReader {
25 paths: StoragePaths,
26 cache: MappedFileCache,
27}
28
29impl FileReader {
30 pub fn new() -> Result<Self> {
31 let paths = StoragePaths::detect()?;
32 Ok(Self::with_paths(paths))
33 }
34
35 pub fn with_paths(paths: StoragePaths) -> Self {
36 Self {
37 paths,
38 cache: MappedFileCache::new(),
39 }
40 }
41
42 pub fn paths(&self) -> &StoragePaths {
43 &self.paths
44 }
45
46 pub fn cache(&self) -> &MappedFileCache {
47 &self.cache
48 }
49
50 pub fn read_mapped(&self, path: &Path) -> Result<Arc<MappedFile>> {
51 self.cache.get(path)
52 }
53
54 pub fn read_span(&self, path: &Path) -> Result<MappedSpan> {
55 let file = self.read_mapped(path)?;
56 let len = file.len();
57 Ok(MappedSpan {
58 file,
59 offset: 0,
60 len,
61 })
62 }
63
64 pub fn parse_span<T: DeserializeOwned>(&self, span: &MappedSpan) -> Result<T> {
65 serde_json::from_slice(span.as_bytes()).map_err(Error::Json)
66 }
67
68 pub fn read_json<T: DeserializeOwned>(&self, path: &Path) -> Result<T> {
69 let span = self.read_span(path)?;
70 self.parse_span(&span)
71 }
72
73 pub fn read_session_span(&self, project_id: &str, id: &SessionId) -> Result<MappedSpan> {
74 let path = self.paths.session_file(project_id, id);
75 self.read_span(&path)
76 }
77
78 pub fn read_message_span(&self, session_id: &SessionId, id: &MessageId) -> Result<MappedSpan> {
79 let path = self.paths.message_file(session_id, id);
80 self.read_span(&path)
81 }
82
83 pub fn read_part_span(&self, message_id: &MessageId, id: &PartId) -> Result<MappedSpan> {
84 let path = self.paths.part_file(message_id, id);
85 self.read_span(&path)
86 }
87
88 pub fn read_diff_span(&self, session_id: &SessionId) -> Result<Option<MappedSpan>> {
89 let path = self.paths.diff_file(session_id);
90 if !path.exists() {
91 return Ok(None);
92 }
93 self.read_span(&path).map(Some)
94 }
95
96 pub fn read_session(&self, project_id: &str, id: &SessionId) -> Result<SessionInfo> {
97 let path = self.paths.session_file(project_id, id);
98 self.read_json(&path)
99 }
100
101 pub fn read_message(&self, session_id: &SessionId, id: &MessageId) -> Result<Message> {
102 let path = self.paths.message_file(session_id, id);
103 self.read_json(&path)
104 }
105
106 pub fn read_part(&self, message_id: &MessageId, id: &PartId) -> Result<Part> {
107 let path = self.paths.part_file(message_id, id);
108 self.read_json(&path)
109 }
110
111 pub fn read_diff(&self, session_id: &SessionId) -> Result<Vec<FileDiff>> {
112 let path = self.paths.diff_file(session_id);
113 if !path.exists() {
114 return Ok(Vec::new());
115 }
116 self.read_json(&path)
117 }
118
119 fn list_ids_in_dir<T, F>(&self, dir: &Path, parse_id: F, descending: bool) -> Result<Vec<T>>
120 where
121 T: AsRef<str>,
122 F: Fn(&str) -> Option<T>,
123 {
124 if !dir.exists() {
125 return Ok(Vec::new());
126 }
127
128 let mut ids = Vec::new();
129 for entry in std::fs::read_dir(dir)? {
130 let entry = entry?;
131 if !entry.file_type()?.is_file() {
132 continue;
133 }
134
135 let path = entry.path();
136 if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
137 continue;
138 }
139
140 let Some(filename) = path.file_name().and_then(|name| name.to_str()) else {
141 continue;
142 };
143
144 if let Some(id) = parse_id(filename) {
145 ids.push(id);
146 }
147 }
148
149 if descending {
150 ids.sort_by(|a, b| b.as_ref().cmp(a.as_ref()));
151 } else {
152 ids.sort_by(|a, b| a.as_ref().cmp(b.as_ref()));
153 }
154
155 Ok(ids)
156 }
157
158 pub fn list_sessions(&self, project_id: &str) -> Result<Vec<SessionId>> {
159 let dir = self.paths.session_dir(project_id);
160 self.list_ids_in_dir(&dir, SessionId::from_filename, true)
161 }
162
163 pub fn list_messages(&self, session_id: &SessionId) -> Result<Vec<MessageId>> {
164 let dir = self.paths.message_dir(session_id);
165 self.list_ids_in_dir(&dir, MessageId::from_filename, false)
166 }
167
168 pub fn list_parts(&self, message_id: &MessageId) -> Result<Vec<PartId>> {
169 let dir = self.paths.part_dir(message_id);
170 self.list_ids_in_dir(&dir, PartId::from_filename, false)
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_reader_creation() {
180 let result = FileReader::new();
181 match result {
182 Ok(_) => {}
183 Err(Error::StorageRootNotFound) => {}
184 Err(e) => panic!("unexpected error: {}", e),
185 }
186 }
187}