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