1use super::{DirEntry, Filesystem};
6use async_trait::async_trait;
7use std::collections::BTreeMap;
8use std::io;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12pub use kaish_types::backend::MountInfo;
16
17#[derive(Default)]
23pub struct VfsRouter {
24 mounts: BTreeMap<PathBuf, Arc<dyn Filesystem>>,
26}
27
28impl std::fmt::Debug for VfsRouter {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.debug_struct("VfsRouter")
31 .field("mounts", &self.mounts.keys().collect::<Vec<_>>())
32 .finish()
33 }
34}
35
36impl VfsRouter {
37 pub fn new() -> Self {
39 Self {
40 mounts: BTreeMap::new(),
41 }
42 }
43
44 pub fn mount(&mut self, path: impl Into<PathBuf>, fs: impl Filesystem + 'static) {
49 let path = Self::normalize_mount_path(path.into());
50 self.mounts.insert(path, Arc::new(fs));
51 }
52
53 pub fn mount_arc(&mut self, path: impl Into<PathBuf>, fs: Arc<dyn Filesystem>) {
55 let path = Self::normalize_mount_path(path.into());
56 self.mounts.insert(path, fs);
57 }
58
59 pub fn unmount(&mut self, path: impl AsRef<Path>) -> bool {
63 let path = Self::normalize_mount_path(path.as_ref().to_path_buf());
64 self.mounts.remove(&path).is_some()
65 }
66
67 pub fn list_mounts(&self) -> Vec<MountInfo> {
69 self.mounts
70 .iter()
71 .map(|(path, fs)| MountInfo {
72 path: path.clone(),
73 read_only: fs.read_only(),
74 })
75 .collect()
76 }
77
78 fn normalize_mount_path(path: PathBuf) -> PathBuf {
80 let s = path.to_string_lossy();
81 let s = s.trim_end_matches('/');
82 if s.is_empty() {
83 PathBuf::from("/")
84 } else if !s.starts_with('/') {
85 PathBuf::from(format!("/{}", s))
86 } else {
87 PathBuf::from(s)
88 }
89 }
90
91 pub fn resolve_real_path(&self, path: &Path) -> Option<PathBuf> {
98 let (fs, relative) = self.find_mount(path).ok()?;
99 fs.real_path(&relative)
100 }
101
102 fn find_mount(&self, path: &Path) -> io::Result<(Arc<dyn Filesystem>, PathBuf)> {
106 let path_str = path.to_string_lossy();
107 let normalized = if path_str.starts_with('/') {
108 path.to_path_buf()
109 } else {
110 PathBuf::from(format!("/{}", path_str))
111 };
112
113 let mut best_match: Option<(&PathBuf, &Arc<dyn Filesystem>)> = None;
115
116 for (mount_path, fs) in &self.mounts {
117 let mount_str = mount_path.to_string_lossy();
118
119 let is_match = if mount_str == "/" {
121 true } else {
123 let normalized_str = normalized.to_string_lossy();
124 normalized_str == mount_str.as_ref()
125 || normalized_str.starts_with(&format!("{}/", mount_str))
126 };
127
128 if is_match {
129 let dominated = best_match
131 .as_ref()
132 .is_none_or(|(bp, _)| mount_path.as_os_str().len() > bp.as_os_str().len());
133 if dominated {
134 best_match = Some((mount_path, fs));
135 }
136 }
137 }
138
139 match best_match {
140 Some((mount_path, fs)) => {
141 let mount_str = mount_path.to_string_lossy();
143 let normalized_str = normalized.to_string_lossy();
144
145 let relative = if mount_str == "/" {
146 normalized_str.trim_start_matches('/').to_string()
147 } else {
148 normalized_str
149 .strip_prefix(mount_str.as_ref())
150 .unwrap_or("")
151 .trim_start_matches('/')
152 .to_string()
153 };
154
155 Ok((Arc::clone(fs), PathBuf::from(relative)))
156 }
157 None => Err(io::Error::new(
158 io::ErrorKind::NotFound,
159 format!("no mount point for path: {}", path.display()),
160 )),
161 }
162 }
163}
164
165#[async_trait]
166impl Filesystem for VfsRouter {
167 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
168 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
169 let (fs, relative) = self.find_mount(path)?;
170 fs.read(&relative).await
171 }
172
173 #[tracing::instrument(level = "trace", skip(self, data), fields(path = %path.display(), size = data.len()))]
174 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
175 let (fs, relative) = self.find_mount(path)?;
176 fs.write(&relative, data).await
177 }
178
179 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
180 async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
181 let path_str = path.to_string_lossy();
183 if path_str.is_empty() || path_str == "/" {
184 return self.list_root().await;
185 }
186
187 let (fs, relative) = self.find_mount(path)?;
188 fs.list(&relative).await
189 }
190
191 #[tracing::instrument(level = "trace", skip(self), fields(path = %path.display()))]
192 async fn stat(&self, path: &Path) -> io::Result<DirEntry> {
193 let path_str = path.to_string_lossy();
195 if path_str.is_empty() || path_str == "/" {
196 return Ok(DirEntry::directory("/"));
197 }
198
199 let normalized = Self::normalize_mount_path(path.to_path_buf());
201 if self.mounts.contains_key(&normalized) {
202 let name = path
203 .file_name()
204 .map(|n| n.to_string_lossy().into_owned())
205 .unwrap_or_else(|| "/".to_string());
206 return Ok(DirEntry::directory(name));
207 }
208
209 let (fs, relative) = self.find_mount(path)?;
210 fs.stat(&relative).await
211 }
212
213 async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
214 let (fs, relative) = self.find_mount(path)?;
215 fs.read_link(&relative).await
216 }
217
218 async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
219 let (fs, relative) = self.find_mount(link)?;
220 fs.symlink(target, &relative).await
221 }
222
223 async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
224 let path_str = path.to_string_lossy();
226 if path_str.is_empty() || path_str == "/" {
227 return Ok(DirEntry::directory("/"));
228 }
229
230 let normalized = Self::normalize_mount_path(path.to_path_buf());
232 if self.mounts.contains_key(&normalized) {
233 let name = path
234 .file_name()
235 .map(|n| n.to_string_lossy().into_owned())
236 .unwrap_or_else(|| "/".to_string());
237 return Ok(DirEntry::directory(name));
238 }
239
240 let (fs, relative) = self.find_mount(path)?;
241 fs.lstat(&relative).await
242 }
243
244 async fn mkdir(&self, path: &Path) -> io::Result<()> {
245 let (fs, relative) = self.find_mount(path)?;
246 fs.mkdir(&relative).await
247 }
248
249 async fn set_mtime(&self, path: &Path, mtime: std::time::SystemTime) -> io::Result<()> {
250 let (fs, relative) = self.find_mount(path)?;
251 fs.set_mtime(&relative, mtime).await
252 }
253
254 async fn remove(&self, path: &Path) -> io::Result<()> {
255 let (fs, relative) = self.find_mount(path)?;
256 fs.remove(&relative).await
257 }
258
259 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
260 let (from_fs, from_relative) = self.find_mount(from)?;
261 let (to_fs, to_relative) = self.find_mount(to)?;
262
263 if !Arc::ptr_eq(&from_fs, &to_fs) {
265 return Err(io::Error::new(
266 io::ErrorKind::Unsupported,
267 "cannot rename across different mount points",
268 ));
269 }
270
271 from_fs.rename(&from_relative, &to_relative).await
272 }
273
274 fn read_only(&self) -> bool {
275 if self.mounts.is_empty() {
279 return false;
280 }
281 self.mounts.values().all(|fs| fs.read_only())
282 }
283}
284
285impl VfsRouter {
286 async fn list_root(&self) -> io::Result<Vec<DirEntry>> {
288 let mut entries = Vec::new();
289 let mut seen_names = std::collections::HashSet::new();
290
291 for mount_path in self.mounts.keys() {
292 let mount_str = mount_path.to_string_lossy();
293 if mount_str == "/" {
294 if let Some(fs) = self.mounts.get(mount_path)
296 && let Ok(root_entries) = fs.list(Path::new("")).await {
297 for entry in root_entries {
298 if seen_names.insert(entry.name.clone()) {
299 entries.push(entry);
300 }
301 }
302 }
303 } else {
304 let first_component = mount_str
306 .trim_start_matches('/')
307 .split('/')
308 .next()
309 .unwrap_or("");
310
311 if !first_component.is_empty() && seen_names.insert(first_component.to_string()) {
312 entries.push(DirEntry::directory(first_component));
313 }
314 }
315 }
316
317 entries.sort_by(|a, b| a.name.cmp(&b.name));
318 Ok(entries)
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::vfs::MemoryFs;
326
327 #[tokio::test]
328 async fn test_basic_mount() {
329 let mut router = VfsRouter::new();
330 let scratch = MemoryFs::new();
331 scratch.write(Path::new("test.txt"), b"hello").await.unwrap();
332 router.mount("/scratch", scratch);
333
334 let data = router.read(Path::new("/scratch/test.txt")).await.unwrap();
335 assert_eq!(data, b"hello");
336 }
337
338 #[tokio::test]
339 async fn test_multiple_mounts() {
340 let mut router = VfsRouter::new();
341
342 let scratch = MemoryFs::new();
343 scratch.write(Path::new("a.txt"), b"scratch").await.unwrap();
344 router.mount("/scratch", scratch);
345
346 let data = MemoryFs::new();
347 data.write(Path::new("b.txt"), b"data").await.unwrap();
348 router.mount("/data", data);
349
350 assert_eq!(
351 router.read(Path::new("/scratch/a.txt")).await.unwrap(),
352 b"scratch"
353 );
354 assert_eq!(
355 router.read(Path::new("/data/b.txt")).await.unwrap(),
356 b"data"
357 );
358 }
359
360 #[tokio::test]
361 async fn test_nested_mount() {
362 let mut router = VfsRouter::new();
363
364 let outer = MemoryFs::new();
365 outer.write(Path::new("outer.txt"), b"outer").await.unwrap();
366 router.mount("/mnt", outer);
367
368 let inner = MemoryFs::new();
369 inner.write(Path::new("inner.txt"), b"inner").await.unwrap();
370 router.mount("/mnt/project", inner);
371
372 assert_eq!(
374 router.read(Path::new("/mnt/outer.txt")).await.unwrap(),
375 b"outer"
376 );
377
378 assert_eq!(
380 router.read(Path::new("/mnt/project/inner.txt")).await.unwrap(),
381 b"inner"
382 );
383 }
384
385 #[tokio::test]
386 async fn test_list_root() {
387 let mut router = VfsRouter::new();
388 router.mount("/scratch", MemoryFs::new());
389 router.mount("/mnt/a", MemoryFs::new());
390 router.mount("/mnt/b", MemoryFs::new());
391
392 let entries = router.list(Path::new("/")).await.unwrap();
393 let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
394
395 assert!(names.contains(&&"scratch".to_string()));
396 assert!(names.contains(&&"mnt".to_string()));
397 }
398
399 #[tokio::test]
400 async fn test_unmount() {
401 let mut router = VfsRouter::new();
402
403 let fs = MemoryFs::new();
404 fs.write(Path::new("test.txt"), b"data").await.unwrap();
405 router.mount("/scratch", fs);
406
407 assert!(router.read(Path::new("/scratch/test.txt")).await.is_ok());
408
409 router.unmount("/scratch");
410
411 assert!(router.read(Path::new("/scratch/test.txt")).await.is_err());
412 }
413
414 #[tokio::test]
415 async fn test_list_mounts() {
416 let mut router = VfsRouter::new();
417 router.mount("/scratch", MemoryFs::new());
418 router.mount("/data", MemoryFs::new());
419
420 let mounts = router.list_mounts();
421 assert_eq!(mounts.len(), 2);
422
423 let paths: Vec<_> = mounts.iter().map(|m| &m.path).collect();
424 assert!(paths.contains(&&PathBuf::from("/scratch")));
425 assert!(paths.contains(&&PathBuf::from("/data")));
426 }
427
428 #[tokio::test]
429 async fn test_no_mount_error() {
430 let router = VfsRouter::new();
431 let result = router.read(Path::new("/nothing/here.txt")).await;
432 assert!(result.is_err());
433 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
434 }
435
436 #[tokio::test]
437 async fn test_root_mount() {
438 let mut router = VfsRouter::new();
439
440 let root = MemoryFs::new();
441 root.write(Path::new("at-root.txt"), b"root file").await.unwrap();
442 router.mount("/", root);
443
444 let data = router.read(Path::new("/at-root.txt")).await.unwrap();
445 assert_eq!(data, b"root file");
446 }
447
448 #[tokio::test]
449 async fn test_write_through_router() {
450 let mut router = VfsRouter::new();
451 router.mount("/scratch", MemoryFs::new());
452
453 router
454 .write(Path::new("/scratch/new.txt"), b"created")
455 .await
456 .unwrap();
457
458 let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
459 assert_eq!(data, b"created");
460 }
461
462 #[tokio::test]
463 async fn test_stat_mount_point() {
464 let mut router = VfsRouter::new();
465 router.mount("/scratch", MemoryFs::new());
466
467 let entry = router.stat(Path::new("/scratch")).await.unwrap();
468 assert!(entry.is_dir());
469 }
470
471 #[tokio::test]
472 async fn test_stat_root() {
473 let router = VfsRouter::new();
474 let entry = router.stat(Path::new("/")).await.unwrap();
475 assert!(entry.is_dir());
476 }
477
478 #[tokio::test]
479 async fn test_rename_same_mount() {
480 let mut router = VfsRouter::new();
481 let mem = MemoryFs::new();
482 mem.write(Path::new("old.txt"), b"data").await.unwrap();
483 router.mount("/scratch", mem);
484
485 router.rename(Path::new("/scratch/old.txt"), Path::new("/scratch/new.txt")).await.unwrap();
486
487 let data = router.read(Path::new("/scratch/new.txt")).await.unwrap();
489 assert_eq!(data, b"data");
490
491 assert!(!router.exists(Path::new("/scratch/old.txt")).await);
493 }
494
495 #[tokio::test]
496 async fn test_rename_cross_mount_fails() {
497 let mut router = VfsRouter::new();
498 let mem1 = MemoryFs::new();
499 mem1.write(Path::new("file.txt"), b"data").await.unwrap();
500 router.mount("/mount1", mem1);
501 router.mount("/mount2", MemoryFs::new());
502
503 let result = router.rename(Path::new("/mount1/file.txt"), Path::new("/mount2/file.txt")).await;
504 assert!(result.is_err());
505 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::Unsupported);
506 }
507
508 #[tokio::test]
509 async fn read_only_empty_router_returns_false() {
510 let router = VfsRouter::new();
511 assert!(!router.read_only());
512 }
513
514 #[cfg(feature = "localfs")]
515 #[tokio::test]
516 async fn read_only_all_read_only_mounts_returns_true() {
517 use crate::vfs::LocalFs;
518
519 let t1 = tempfile::tempdir().unwrap();
520 let t2 = tempfile::tempdir().unwrap();
521
522 let mut router = VfsRouter::new();
523 router.mount("/a", LocalFs::read_only(t1.path().to_path_buf()));
524 router.mount("/b", LocalFs::read_only(t2.path().to_path_buf()));
525
526 assert!(router.read_only());
527 }
528
529 #[cfg(feature = "localfs")]
530 #[tokio::test]
531 async fn read_only_mixed_mounts_returns_false() {
532 use crate::vfs::LocalFs;
533
534 let t1 = tempfile::tempdir().unwrap();
535
536 let mut router = VfsRouter::new();
537 router.mount("/ro", LocalFs::read_only(t1.path().to_path_buf()));
538 router.mount("/rw", MemoryFs::new());
539
540 assert!(!router.read_only());
541 }
542}