Real-time index of opencode sessions
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 187 lines 5.3 kB view raw
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}