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, 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    /// Append data to a file, creating it if it doesn't exist.
42    ///
43    /// The default composes `read` (treating a missing file as empty) with
44    /// `write` of the concatenation, which is correct for any backend but
45    /// costs a read permission the caller may not have and is not atomic —
46    /// a writer landing between the read and the write is silently
47    /// overwritten. Backends that can answer a true `O_APPEND`-style append
48    /// — no read, one atomic write — override this to grant it. Backends
49    /// that must materialize state on first write (a copy-on-write overlay
50    /// snapshotting its base) should keep the default: it routes through
51    /// `write`, so materialization still happens correctly.
52    ///
53    /// Returns `Err` if the filesystem is read-only.
54    async fn append(&self, path: &Path, data: &[u8]) -> io::Result<()> {
55        let mut existing = match self.read(path).await {
56            Ok(content) => content,
57            Err(e) if e.kind() == io::ErrorKind::NotFound => Vec::new(),
58            Err(e) => return Err(e),
59        };
60        existing.extend_from_slice(data);
61        self.write(path, &existing).await
62    }
63
64    /// List entries in a directory.
65    async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
66
67    /// Get metadata for a file or directory.
68    async fn stat(&self, path: &Path) -> io::Result<DirEntry>;
69
70    /// Create a directory (and parent directories if needed).
71    ///
72    /// Returns `Err` if the filesystem is read-only.
73    async fn mkdir(&self, path: &Path) -> io::Result<()>;
74
75    /// Remove a file or empty directory.
76    ///
77    /// Returns `Err` if the filesystem is read-only.
78    async fn remove(&self, path: &Path) -> io::Result<()>;
79
80    /// Set the modification time of an existing path.
81    ///
82    /// The default errors with `Unsupported`. Writable filesystems that track
83    /// timestamps override this; read-only mounts reject. There is deliberately
84    /// **no silent no-op** — a `touch` that cannot record the time must say so
85    /// rather than report success it didn't deliver.
86    async fn set_mtime(&self, path: &Path, mtime: SystemTime) -> io::Result<()> {
87        let _ = mtime;
88        Err(io::Error::new(
89            io::ErrorKind::Unsupported,
90            format!("set_mtime not supported for {}", path.display()),
91        ))
92    }
93
94    /// Returns true if this filesystem is read-only.
95    fn read_only(&self) -> bool;
96
97    /// Memory-resident content bytes this filesystem is holding, if it
98    /// tracks them.
99    ///
100    /// Memory-backed filesystems (`MemoryFs`, `OverlayFs` and its base
101    /// snapshots) keep an exact net counter — an overwrite charges the
102    /// delta, a remove credits — and return `Some`. Disk-backed filesystems
103    /// keep the default `None`: disk residency is the host's concern (page
104    /// cache, `df`); this counter is about RAM. Counts file content only,
105    /// not directory/symlink metadata. Feeds per-mount introspection and
106    /// eviction decisions.
107    fn resident_bytes(&self) -> Option<u64> {
108        None
109    }
110
111    /// Check if a path exists.
112    async fn exists(&self, path: &Path) -> bool {
113        self.stat(path).await.is_ok()
114    }
115
116    /// Rename (move) a file or directory.
117    ///
118    /// This is an atomic operation when source and destination are on the same
119    /// filesystem. The default implementation falls back to copy+delete, which
120    /// is not atomic.
121    ///
122    /// Returns `Err` if the filesystem is read-only.
123    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
124        // Default implementation: copy then delete (not atomic)
125        let entry = self.stat(from).await?;
126        if entry.is_dir() {
127            // For directories, we'd need recursive copy - just error for now
128            return Err(io::Error::new(
129                io::ErrorKind::Unsupported,
130                "rename directories not supported by this filesystem",
131            ));
132        }
133        let data = self.read(from).await?;
134        self.write(to, &data).await?;
135        self.remove(from).await?;
136        Ok(())
137    }
138
139    /// Get the real filesystem path for a VFS path.
140    ///
141    /// Returns `Some(path)` for backends backed by the real filesystem (like LocalFs),
142    /// or `None` for virtual backends (like MemoryFs).
143    ///
144    /// This is needed for tools like `git` that must use real paths with external libraries.
145    fn real_path(&self, path: &Path) -> Option<PathBuf> {
146        let _ = path;
147        None
148    }
149
150    /// Read the target of a symbolic link without following it.
151    ///
152    /// Returns the path the symlink points to. Use `stat` to follow symlinks.
153    async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
154        let _ = path;
155        Err(io::Error::new(
156            io::ErrorKind::InvalidInput,
157            "symlinks not supported by this filesystem",
158        ))
159    }
160
161    /// Create a symbolic link.
162    ///
163    /// Creates a symlink at `link` pointing to `target`. The target path
164    /// is stored as-is (may be relative or absolute).
165    async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
166        let _ = (target, link);
167        Err(io::Error::new(
168            io::ErrorKind::InvalidInput,
169            "symlinks not supported by this filesystem",
170        ))
171    }
172
173    /// Get metadata for a path without following symlinks.
174    ///
175    /// Unlike `stat`, this returns metadata about the symlink itself,
176    /// not the target it points to.
177    async fn lstat(&self, path: &Path) -> io::Result<DirEntry> {
178        // Default: same as stat (for backends that don't support symlinks)
179        self.stat(path).await
180    }
181}