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