agentos_vfs/local/
file_block_store.rs1use async_trait::async_trait;
2use std::collections::{HashMap, VecDeque};
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6use vfs::engine::block::BlockStore;
7use vfs::engine::error::{VfsError, VfsResult};
8use vfs::engine::types::BlockKey;
9
10#[derive(Debug, Clone)]
11pub struct FileBlockStore {
12 root: PathBuf,
13 cache: Arc<Mutex<BlockCache>>,
14}
15
16const DEFAULT_BLOCK_CACHE_BYTES: usize = 16 * 1024 * 1024;
17
18#[derive(Debug)]
19struct BlockCache {
20 entries: HashMap<BlockKey, Vec<u8>>,
21 insertion_order: VecDeque<BlockKey>,
22 bytes: usize,
23 max_bytes: usize,
24}
25
26impl BlockCache {
27 fn new(max_bytes: usize) -> Self {
28 Self {
29 entries: HashMap::new(),
30 insertion_order: VecDeque::new(),
31 bytes: 0,
32 max_bytes,
33 }
34 }
35
36 fn get(&self, key: &BlockKey) -> Option<Vec<u8>> {
37 self.entries.get(key).cloned()
38 }
39
40 fn contains(&self, key: &BlockKey) -> bool {
41 self.entries.contains_key(key)
42 }
43
44 fn insert(&mut self, key: BlockKey, data: &[u8]) {
45 if data.len() > self.max_bytes {
46 return;
47 }
48 if let Some(previous) = self.entries.insert(key.clone(), data.to_vec()) {
49 self.bytes = self.bytes.saturating_sub(previous.len());
50 } else {
51 self.insertion_order.push_back(key);
52 }
53 self.bytes = self.bytes.saturating_add(data.len());
54 while self.bytes > self.max_bytes {
55 let Some(oldest) = self.insertion_order.pop_front() else {
56 break;
57 };
58 if let Some(removed) = self.entries.remove(&oldest) {
59 self.bytes = self.bytes.saturating_sub(removed.len());
60 }
61 }
62 }
63
64 fn remove(&mut self, key: &BlockKey) {
65 if let Some(removed) = self.entries.remove(key) {
66 self.bytes = self.bytes.saturating_sub(removed.len());
67 }
68 }
69}
70
71impl FileBlockStore {
72 pub fn new(root: impl Into<PathBuf>) -> VfsResult<Self> {
73 let root = root.into();
74 fs::create_dir_all(&root)
75 .map_err(|err| VfsError::eio(format!("create block root {}: {err}", root.display())))?;
76 Ok(Self {
77 root,
78 cache: Arc::new(Mutex::new(BlockCache::new(DEFAULT_BLOCK_CACHE_BYTES))),
79 })
80 }
81
82 fn path_for(&self, key: &BlockKey) -> PathBuf {
83 block_path(&self.root, key)
84 }
85
86 fn ensure_safe_key(key: &BlockKey) -> VfsResult<()> {
87 if key.0.contains('/') || key.0.contains('\\') || key.0 == "." || key.0 == ".." {
88 return Err(VfsError::einval(format!("unsafe block key: {}", key.0)));
89 }
90 Ok(())
91 }
92
93 pub fn root(&self) -> &Path {
94 &self.root
95 }
96}
97
98#[async_trait]
99impl BlockStore for FileBlockStore {
100 async fn get(&self, key: &BlockKey) -> VfsResult<Vec<u8>> {
101 Self::ensure_safe_key(key)?;
102 if let Some(data) = self
103 .cache
104 .lock()
105 .expect("block cache mutex poisoned")
106 .get(key)
107 {
108 return Ok(data);
109 }
110 let path = self.path_for(key);
111 let data = fs::read(&path).map_err(|err| {
112 if err.kind() == std::io::ErrorKind::NotFound {
113 VfsError::enoent(&key.0)
114 } else {
115 VfsError::eio(format!("read block {}: {err}", path.display()))
116 }
117 })?;
118 self.cache
119 .lock()
120 .expect("block cache mutex poisoned")
121 .insert(key.clone(), &data);
122 Ok(data)
123 }
124
125 async fn get_range(&self, key: &BlockKey, off: u64, len: u64) -> VfsResult<Vec<u8>> {
126 let data = self.get(key).await?;
127 let start = usize::try_from(off)
128 .map_err(|_| VfsError::einval(format!("range offset is too large: {off}")))?;
129 let len = usize::try_from(len)
130 .map_err(|_| VfsError::einval(format!("range length is too large: {len}")))?;
131 if start >= data.len() {
132 return Ok(Vec::new());
133 }
134 let end = start.saturating_add(len).min(data.len());
135 Ok(data[start..end].to_vec())
136 }
137
138 async fn put(&self, key: &BlockKey, data: &[u8]) -> VfsResult<()> {
139 Self::ensure_safe_key(key)?;
140 let path = self.path_for(key);
141 if let Some(parent) = path.parent() {
142 fs::create_dir_all(parent)
143 .map_err(|err| VfsError::eio(format!("create block dir: {err}")))?;
144 }
145 fs::write(&path, data)
146 .map_err(|err| VfsError::eio(format!("write block {}: {err}", path.display())))?;
147 self.cache
148 .lock()
149 .expect("block cache mutex poisoned")
150 .insert(key.clone(), data);
151 Ok(())
152 }
153
154 async fn exists(&self, key: &BlockKey) -> VfsResult<bool> {
155 Self::ensure_safe_key(key)?;
156 if self
157 .cache
158 .lock()
159 .expect("block cache mutex poisoned")
160 .contains(key)
161 {
162 return Ok(true);
163 }
164 Ok(self.path_for(key).exists())
165 }
166
167 async fn delete_many(&self, keys: &[BlockKey]) -> VfsResult<()> {
168 let mut errors = Vec::new();
169 for key in keys {
170 if let Err(error) = Self::ensure_safe_key(key) {
171 errors.push(error.to_string());
172 continue;
173 }
174 self.cache
175 .lock()
176 .expect("block cache mutex poisoned")
177 .remove(key);
178 match fs::remove_file(self.path_for(key)) {
179 Ok(()) => {}
180 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
181 Err(err) => errors.push(format!("delete block {}: {err}", key.0)),
182 }
183 }
184 if errors.is_empty() {
185 Ok(())
186 } else {
187 Err(VfsError::eio(format!(
188 "delete {} local blocks failed: {}",
189 errors.len(),
190 errors.join("; ")
191 )))
192 }
193 }
194
195 async fn copy(&self, src: &BlockKey, dst: &BlockKey) -> VfsResult<()> {
196 let data = self.get(src).await?;
197 self.put(dst, &data).await
198 }
199
200 async fn sync(&self) -> VfsResult<()> {
201 for prefix in fs::read_dir(&self.root).map_err(|err| {
202 VfsError::eio(format!("read block root {}: {err}", self.root.display()))
203 })? {
204 let prefix = prefix.map_err(|err| {
205 VfsError::eio(format!(
206 "read block root entry {}: {err}",
207 self.root.display()
208 ))
209 })?;
210 let file_type = prefix.file_type().map_err(|err| {
211 VfsError::eio(format!(
212 "stat block entry {}: {err}",
213 prefix.path().display()
214 ))
215 })?;
216 if !file_type.is_dir() {
217 continue;
218 }
219 for block in fs::read_dir(prefix.path()).map_err(|err| {
220 VfsError::eio(format!(
221 "read block directory {}: {err}",
222 prefix.path().display()
223 ))
224 })? {
225 let block = block
226 .map_err(|err| VfsError::eio(format!("read block directory entry: {err}")))?;
227 if block
228 .file_type()
229 .map_err(|err| {
230 VfsError::eio(format!("stat block {}: {err}", block.path().display()))
231 })?
232 .is_file()
233 {
234 fs::File::open(block.path())
235 .and_then(|file| file.sync_all())
236 .map_err(|err| {
237 VfsError::eio(format!("sync block {}: {err}", block.path().display()))
238 })?;
239 }
240 }
241 fs::File::open(prefix.path())
242 .and_then(|directory| directory.sync_all())
243 .map_err(|err| {
244 VfsError::eio(format!(
245 "sync block directory {}: {err}",
246 prefix.path().display()
247 ))
248 })?;
249 }
250 fs::File::open(&self.root)
251 .and_then(|directory| directory.sync_all())
252 .map_err(|err| VfsError::eio(format!("sync block root {}: {err}", self.root.display())))
253 }
254}
255
256fn block_path(root: &Path, key: &BlockKey) -> PathBuf {
257 let (prefix, suffix) = key.0.split_at(key.0.len().min(2));
258 root.join(prefix).join(suffix)
259}