vv_agent/workspace/
memory.rs1use std::any::Any;
2use std::collections::{BTreeMap, BTreeSet};
3use std::sync::{Arc, Mutex};
4
5use super::{
6 current_utc_isoformat, glob_match, insert_parent_dirs, normalize_workspace_path,
7 normalized_glob_pattern, not_found, suffix_with_dot, FileInfo, WorkspaceBackend,
8};
9
10#[derive(Debug, Clone)]
11pub struct MemoryWorkspaceBackend {
12 files: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
13 dirs: Arc<Mutex<BTreeSet<String>>>,
14}
15
16impl Default for MemoryWorkspaceBackend {
17 fn default() -> Self {
18 let mut dirs = BTreeSet::new();
19 dirs.insert(String::new());
20 Self {
21 files: Arc::new(Mutex::new(BTreeMap::new())),
22 dirs: Arc::new(Mutex::new(dirs)),
23 }
24 }
25}
26
27impl WorkspaceBackend for MemoryWorkspaceBackend {
28 fn as_any(&self) -> &dyn Any {
29 self
30 }
31
32 fn list_files(&self, base: &str, glob: &str) -> std::io::Result<Vec<String>> {
33 let base = normalize_workspace_path(base);
34 let glob = normalized_glob_pattern(glob);
35 let pattern = if base.is_empty() {
36 glob
37 } else {
38 format!("{base}/{glob}")
39 };
40 let files = self.files.lock().expect("memory workspace poisoned");
41 let mut matches = files
42 .keys()
43 .filter(|path| glob_match(path, &pattern))
44 .cloned()
45 .collect::<Vec<_>>();
46 matches.sort();
47 Ok(matches)
48 }
49
50 fn read_text(&self, path: &str) -> std::io::Result<String> {
51 let bytes = self.read_bytes(path)?;
52 Ok(String::from_utf8_lossy(&bytes).to_string())
53 }
54
55 fn read_bytes(&self, path: &str) -> std::io::Result<Vec<u8>> {
56 let key = normalize_workspace_path(path);
57 let files = self.files.lock().expect("memory workspace poisoned");
58 files.get(&key).cloned().ok_or_else(|| not_found(path))
59 }
60
61 fn write_text(&self, path: &str, content: &str, append: bool) -> std::io::Result<usize> {
62 let key = normalize_workspace_path(path);
63 let mut files = self.files.lock().expect("memory workspace poisoned");
64 let entry = files.entry(key.clone()).or_default();
65 if append {
66 entry.extend_from_slice(content.as_bytes());
67 } else {
68 *entry = content.as_bytes().to_vec();
69 }
70 drop(files);
71 self.ensure_parent_dirs(&key);
72 Ok(content.len())
73 }
74
75 fn file_info(&self, path: &str) -> std::io::Result<Option<FileInfo>> {
76 let key = normalize_workspace_path(path);
77 let files = self.files.lock().expect("memory workspace poisoned");
78 if let Some(content) = files.get(&key) {
79 return Ok(Some(FileInfo {
80 path: key,
81 is_file: true,
82 is_dir: false,
83 size: content.len() as u64,
84 modified_at: current_utc_isoformat(),
85 suffix: suffix_with_dot(path),
86 }));
87 }
88 drop(files);
89 let dirs = self.dirs.lock().expect("memory workspace poisoned");
90 Ok(dirs.get(&key).map(|path| FileInfo {
91 path: if path.is_empty() {
92 ".".to_string()
93 } else {
94 path.clone()
95 },
96 is_file: false,
97 is_dir: true,
98 size: 0,
99 modified_at: current_utc_isoformat(),
100 suffix: String::new(),
101 }))
102 }
103
104 fn exists(&self, path: &str) -> bool {
105 let key = normalize_workspace_path(path);
106 self.files
107 .lock()
108 .expect("memory workspace poisoned")
109 .contains_key(&key)
110 || self
111 .dirs
112 .lock()
113 .expect("memory workspace poisoned")
114 .contains(&key)
115 }
116
117 fn is_file(&self, path: &str) -> bool {
118 let key = normalize_workspace_path(path);
119 self.files
120 .lock()
121 .expect("memory workspace poisoned")
122 .contains_key(&key)
123 }
124
125 fn mkdir(&self, path: &str) -> std::io::Result<()> {
126 let key = normalize_workspace_path(path);
127 let mut dirs = self.dirs.lock().expect("memory workspace poisoned");
128 dirs.insert(key.clone());
129 insert_parent_dirs(&mut dirs, &key);
130 Ok(())
131 }
132}
133
134impl MemoryWorkspaceBackend {
135 fn ensure_parent_dirs(&self, key: &str) {
136 let mut dirs = self.dirs.lock().expect("memory workspace poisoned");
137 insert_parent_dirs(&mut dirs, key);
138 }
139}