Skip to main content

kaish_vfs/
traits.rs

1//! Core VFS traits and types.
2
3use async_trait::async_trait;
4use std::io;
5use std::path::{Path, PathBuf};
6use std::time::SystemTime;
7
8// DirEntry and DirEntryKind live in kaish-types.
9pub use kaish_types::{DirEntry, DirEntryKind};
10
11/// Abstract filesystem interface.
12///
13/// All operations use paths relative to the filesystem root.
14/// For example, if a `LocalFs` is rooted at `/home/amy/project`,
15/// then `read("src/main.rs")` reads `/home/amy/project/src/main.rs`.
16#[async_trait]
17pub trait Filesystem: Send + Sync {
18    /// Read the entire contents of a file.
19    async fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
20
21    /// Write data to a file, creating it if it doesn't exist.
22    ///
23    /// Returns `Err` if the filesystem is read-only.
24    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
25
26    /// List entries in a directory.
27    async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
28
29    /// Get metadata for a file or directory.
30    async fn stat(&self, path: &Path) -> io::Result<DirEntry>;
31
32    /// Create a directory (and parent directories if needed).
33    ///
34    /// Returns `Err` if the filesystem is read-only.
35    async fn mkdir(&self, path: &Path) -> io::Result<()>;
36
37    /// Remove a file or empty directory.
38    ///
39    /// Returns `Err` if the filesystem is read-only.
40    async fn remove(&self, path: &Path) -> io::Result<()>;
41
42    /// Set the modification time of an existing path.
43    ///
44    /// The default errors with `Unsupported`. Writable filesystems that track
45    /// timestamps override this; read-only mounts reject. There is deliberately
46    /// **no silent no-op** — a `touch` that cannot record the time must say so
47    /// rather than report success it didn't deliver.
48    async fn set_mtime(&self, path: &Path, mtime: SystemTime) -> io::Result<()> {
49        let _ = mtime;
50        Err(io::Error::new(
51            io::ErrorKind::Unsupported,
52            format!("set_mtime not supported for {}", path.display()),
53        ))
54    }
55
56    /// Returns true if this filesystem is read-only.
57    fn read_only(&self) -> bool;
58
59    /// Check if a path exists.
60    async fn exists(&self, path: &Path) -> bool {
61        self.stat(path).await.is_ok()
62    }
63
64    /// Rename (move) a file or directory.
65    ///
66    /// This is an atomic operation when source and destination are on the same
67    /// filesystem. The default implementation falls back to copy+delete, which
68    /// is not atomic.
69    ///
70    /// Returns `Err` if the filesystem is read-only.
71    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
72        // Default implementation: copy then delete (not atomic)
73        let entry = self.stat(from).await?;
74        if entry.is_dir() {
75            // For directories, we'd need recursive copy - just error for now
76            return Err(io::Error::new(
77                io::ErrorKind::Unsupported,
78                "rename directories not supported by this filesystem",
79            ));
80        }
81        let data = self.read(from).await?;
82        self.write(to, &data).await?;
83        self.remove(from).await?;
84        Ok(())
85    }
86
87    /// Get the real filesystem path for a VFS path.
88    ///
89    /// Returns `Some(path)` for backends backed by the real filesystem (like LocalFs),
90    /// or `None` for virtual backends (like MemoryFs).
91    ///
92    /// This is needed for tools like `git` that must use real paths with external libraries.
93    fn real_path(&self, path: &Path) -> Option<PathBuf> {
94        let _ = path;
95        None
96    }
97
98    /// Read the target of a symbolic link without following it.
99    ///
100    /// Returns the path the symlink points to. Use `stat` to follow symlinks.
101    async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
102        let _ = path;
103        Err(io::Error::new(
104            io::ErrorKind::InvalidInput,
105            "symlinks not supported by this filesystem",
106        ))
107    }
108
109    /// Create a symbolic link.
110    ///
111    /// Creates a symlink at `link` pointing to `target`. The target path
112    /// is stored as-is (may be relative or absolute).
113    async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
114        let _ = (target, link);
115        Err(io::Error::new(
116            io::ErrorKind::InvalidInput,
117            "symlinks not supported by this filesystem",
118        ))
119    }
120
121    /// Get metadata for a path without following symlinks.
122    ///
123    /// Unlike `stat`, this returns metadata about the symlink itself,
124    /// not the target it points to.
125    async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
126        // Default: same as stat (for backends that don't support symlinks)
127        self.stat(path).await
128    }
129}