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, ReadRange};
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 /// Read a (possibly partial) slice of a file.
22 ///
23 /// The default reads the whole file and slices in memory, which is correct
24 /// for any finite backend. Backends that cannot answer a whole-file read —
25 /// notably synthetic infinite devices like `/dev/zero`, where reading
26 /// "everything" is unbounded — override this to honour the requested byte
27 /// count directly and to reject a `None` range loudly rather than hang.
28 async fn read_range(&self, path: &Path, range: Option<ReadRange>) -> io::Result<Vec<u8>> {
29 let content = self.read(path).await?;
30 Ok(match range {
31 Some(r) => r.apply(&content),
32 None => content,
33 })
34 }
35
36 /// Write data to a file, creating it if it doesn't exist.
37 ///
38 /// Returns `Err` if the filesystem is read-only.
39 async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
40
41 /// List entries in a directory.
42 async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
43
44 /// Get metadata for a file or directory.
45 async fn stat(&self, path: &Path) -> io::Result<DirEntry>;
46
47 /// Create a directory (and parent directories if needed).
48 ///
49 /// Returns `Err` if the filesystem is read-only.
50 async fn mkdir(&self, path: &Path) -> io::Result<()>;
51
52 /// Remove a file or empty directory.
53 ///
54 /// Returns `Err` if the filesystem is read-only.
55 async fn remove(&self, path: &Path) -> io::Result<()>;
56
57 /// Set the modification time of an existing path.
58 ///
59 /// The default errors with `Unsupported`. Writable filesystems that track
60 /// timestamps override this; read-only mounts reject. There is deliberately
61 /// **no silent no-op** — a `touch` that cannot record the time must say so
62 /// rather than report success it didn't deliver.
63 async fn set_mtime(&self, path: &Path, mtime: SystemTime) -> io::Result<()> {
64 let _ = mtime;
65 Err(io::Error::new(
66 io::ErrorKind::Unsupported,
67 format!("set_mtime not supported for {}", path.display()),
68 ))
69 }
70
71 /// Returns true if this filesystem is read-only.
72 fn read_only(&self) -> bool;
73
74 /// Memory-resident content bytes this filesystem is holding, if it
75 /// tracks them.
76 ///
77 /// Memory-backed filesystems (`MemoryFs`, `OverlayFs` and its base
78 /// snapshots) keep an exact net counter — an overwrite charges the
79 /// delta, a remove credits — and return `Some`. Disk-backed filesystems
80 /// keep the default `None`: disk residency is the host's concern (page
81 /// cache, `df`); this counter is about RAM. Counts file content only,
82 /// not directory/symlink metadata. Feeds per-mount introspection and
83 /// eviction decisions.
84 fn resident_bytes(&self) -> Option<u64> {
85 None
86 }
87
88 /// Check if a path exists.
89 async fn exists(&self, path: &Path) -> bool {
90 self.stat(path).await.is_ok()
91 }
92
93 /// Rename (move) a file or directory.
94 ///
95 /// This is an atomic operation when source and destination are on the same
96 /// filesystem. The default implementation falls back to copy+delete, which
97 /// is not atomic.
98 ///
99 /// Returns `Err` if the filesystem is read-only.
100 async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
101 // Default implementation: copy then delete (not atomic)
102 let entry = self.stat(from).await?;
103 if entry.is_dir() {
104 // For directories, we'd need recursive copy - just error for now
105 return Err(io::Error::new(
106 io::ErrorKind::Unsupported,
107 "rename directories not supported by this filesystem",
108 ));
109 }
110 let data = self.read(from).await?;
111 self.write(to, &data).await?;
112 self.remove(from).await?;
113 Ok(())
114 }
115
116 /// Get the real filesystem path for a VFS path.
117 ///
118 /// Returns `Some(path)` for backends backed by the real filesystem (like LocalFs),
119 /// or `None` for virtual backends (like MemoryFs).
120 ///
121 /// This is needed for tools like `git` that must use real paths with external libraries.
122 fn real_path(&self, path: &Path) -> Option<PathBuf> {
123 let _ = path;
124 None
125 }
126
127 /// Read the target of a symbolic link without following it.
128 ///
129 /// Returns the path the symlink points to. Use `stat` to follow symlinks.
130 async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
131 let _ = path;
132 Err(io::Error::new(
133 io::ErrorKind::InvalidInput,
134 "symlinks not supported by this filesystem",
135 ))
136 }
137
138 /// Create a symbolic link.
139 ///
140 /// Creates a symlink at `link` pointing to `target`. The target path
141 /// is stored as-is (may be relative or absolute).
142 async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
143 let _ = (target, link);
144 Err(io::Error::new(
145 io::ErrorKind::InvalidInput,
146 "symlinks not supported by this filesystem",
147 ))
148 }
149
150 /// Get metadata for a path without following symlinks.
151 ///
152 /// Unlike `stat`, this returns metadata about the symlink itself,
153 /// not the target it points to.
154 async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
155 // Default: same as stat (for backends that don't support symlinks)
156 self.stat(path).await
157 }
158}