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 /// Memory-resident content bytes this filesystem is holding, if it
60 /// tracks them.
61 ///
62 /// Memory-backed filesystems (`MemoryFs`, `OverlayFs` and its base
63 /// snapshots) keep an exact net counter — an overwrite charges the
64 /// delta, a remove credits — and return `Some`. Disk-backed filesystems
65 /// keep the default `None`: disk residency is the host's concern (page
66 /// cache, `df`); this counter is about RAM. Counts file content only,
67 /// not directory/symlink metadata. Feeds per-mount introspection and
68 /// eviction decisions.
69 fn resident_bytes(&self) -> Option<u64> {
70 None
71 }
72
73 /// Check if a path exists.
74 async fn exists(&self, path: &Path) -> bool {
75 self.stat(path).await.is_ok()
76 }
77
78 /// Rename (move) a file or directory.
79 ///
80 /// This is an atomic operation when source and destination are on the same
81 /// filesystem. The default implementation falls back to copy+delete, which
82 /// is not atomic.
83 ///
84 /// Returns `Err` if the filesystem is read-only.
85 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
86 // Default implementation: copy then delete (not atomic)
87 let entry = self.stat(from).await?;
88 if entry.is_dir() {
89 // For directories, we'd need recursive copy - just error for now
90 return Err(io::Error::new(
91 io::ErrorKind::Unsupported,
92 "rename directories not supported by this filesystem",
93 ));
94 }
95 let data = self.read(from).await?;
96 self.write(to, &data).await?;
97 self.remove(from).await?;
98 Ok(())
99 }
100
101 /// Get the real filesystem path for a VFS path.
102 ///
103 /// Returns `Some(path)` for backends backed by the real filesystem (like LocalFs),
104 /// or `None` for virtual backends (like MemoryFs).
105 ///
106 /// This is needed for tools like `git` that must use real paths with external libraries.
107 fn real_path(&self, path: &Path) -> Option<PathBuf> {
108 let _ = path;
109 None
110 }
111
112 /// Read the target of a symbolic link without following it.
113 ///
114 /// Returns the path the symlink points to. Use `stat` to follow symlinks.
115 async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
116 let _ = path;
117 Err(io::Error::new(
118 io::ErrorKind::InvalidInput,
119 "symlinks not supported by this filesystem",
120 ))
121 }
122
123 /// Create a symbolic link.
124 ///
125 /// Creates a symlink at `link` pointing to `target`. The target path
126 /// is stored as-is (may be relative or absolute).
127 async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
128 let _ = (target, link);
129 Err(io::Error::new(
130 io::ErrorKind::InvalidInput,
131 "symlinks not supported by this filesystem",
132 ))
133 }
134
135 /// Get metadata for a path without following symlinks.
136 ///
137 /// Unlike `stat`, this returns metadata about the symlink itself,
138 /// not the target it points to.
139 async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
140 // Default: same as stat (for backends that don't support symlinks)
141 self.stat(path).await
142 }
143}