prov_graph/fs.rs
1//! prov's filesystem port.
2//!
3//! prov is generic over *where* documents live. Rather than depend on any
4//! one concrete backend — `std::fs`, `tokio::fs`, or a browser filesystem like
5//! OPFS/IndexedDB — the library asks only for a small async trait that mirrors
6//! the slice of [`std::fs`] its scan/traverse engine needs. Integrators
7//! implement [`ReadStorage`] over whatever backend they have; the workspace
8//! never learns which one.
9//!
10//! This is the classic *ports and adapters* seam. The trait uses native
11//! `async fn` (no boxed futures) because [`Graph`](crate::graph::Graph) is
12//! generic over its backend rather than erased to `dyn`, so callers keep the
13//! backend's real future types and their `Send`-ness. A backend whose futures
14//! are `Send` composes into multithreaded runtimes unchanged.
15//!
16//! The method set mirrors [`std::fs`] names exactly so an adapter is mechanical
17//! to write.
18//!
19//! Only the read half is here. The write half — `Storage`, the durability
20//! vocabulary, and the writable [`StdFs`]/in-memory adapters — is
21//! `prov-store`'s `fs` module, so that depending on this crate cannot get you
22//! the ability to change a workspace.
23
24use std::io;
25use std::path::{Path, PathBuf};
26use std::sync::Arc;
27use std::time::SystemTime;
28
29/// The read half of an async filesystem backend: everything the traversal core
30/// needs, and nothing that can change a byte on disk.
31///
32/// This is the trait [`crate::graph`] is generic over. The split is not
33/// decoration — it is what lets the read core be depended on by a consumer that
34/// must not, and cannot, write: a language server, a renderer, a browser
35/// viewer. A backend that implements only this is a *provably* read-only
36/// workspace, checked by the compiler rather than by review.
37///
38/// Each method mirrors the [`std::fs`] function of the same name.
39/// [`try_exists`] has a default in terms of [`metadata`].
40///
41/// [`try_exists`]: ReadStorage::try_exists
42/// [`metadata`]: ReadStorage::metadata
43pub trait ReadStorage {
44 /// Read the entire contents of a file as bytes. Mirrors [`std::fs::read`].
45 fn read(&self, path: &Path) -> impl Future<Output = io::Result<Vec<u8>>>;
46
47 /// Read the entire contents of a file as a string. Mirrors
48 /// [`std::fs::read_to_string`].
49 fn read_to_string(&self, path: &Path) -> impl Future<Output = io::Result<String>>;
50
51 /// Return the entries in a directory (non-recursive). Mirrors
52 /// [`std::fs::read_dir`], but yields a `Vec` since async iterators are not
53 /// yet stable.
54 fn read_dir(&self, path: &Path) -> impl Future<Output = io::Result<Vec<DirEntry>>>;
55
56 /// Return metadata about the entry at `path`. Mirrors
57 /// [`std::fs::metadata`]; follows symlinks.
58 fn metadata(&self, path: &Path) -> impl Future<Output = io::Result<Metadata>>;
59
60 /// Returns `Ok(true)` if the path exists, `Ok(false)` if it does not, and
61 /// `Err(_)` if the check itself failed. Mirrors `std::fs::try_exists`.
62 fn try_exists(&self, path: &Path) -> impl Future<Output = io::Result<bool>> {
63 async move {
64 match self.metadata(path).await {
65 Ok(_) => Ok(true),
66 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
67 Err(e) => Err(e),
68 }
69 }
70 }
71}
72
73/// A borrowed [`ReadStorage`] is itself a [`ReadStorage`] — so an owned backend
74/// can be lent to something generic over `S: ReadStorage` (e.g. a temporary
75/// [`Graph`](crate::graph::Graph)) without moving it or wrapping it in an
76/// `Arc` the caller doesn't otherwise need.
77///
78/// Every member is forwarded explicitly. `prov-store`'s matching `Storage`
79/// forwarding does the same for the durability members, where leaving any to
80/// inherit the trait's defaults would silently downgrade a real backend's
81/// guarantees the moment it was borrowed.
82impl<S: ReadStorage + ?Sized> ReadStorage for &S {
83 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
84 (**self).read(path).await
85 }
86
87 async fn read_to_string(&self, path: &Path) -> io::Result<String> {
88 (**self).read_to_string(path).await
89 }
90
91 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
92 (**self).read_dir(path).await
93 }
94
95 async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
96 (**self).metadata(path).await
97 }
98
99 async fn try_exists(&self, path: &Path) -> io::Result<bool> {
100 (**self).try_exists(path).await
101 }
102}
103
104/// An `Arc<S>` is itself a [`ReadStorage`] on the same terms as `&S` above — so
105/// a backend shared across several owners (several open `Workspace`s, a
106/// multi-tab web client) still carries its real capabilities through the
107/// `Arc`, rather than an adapter that forgot to unwrap it silently degrading
108/// to the pessimistic defaults.
109///
110/// `Arc<S>` derefs to `S` exactly like `&S` does, so the same explicit,
111/// every-member forwarding applies for the same reason: the trait's defaults
112/// must never be reached by accident.
113impl<S: ReadStorage + ?Sized> ReadStorage for Arc<S> {
114 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
115 (**self).read(path).await
116 }
117
118 async fn read_to_string(&self, path: &Path) -> io::Result<String> {
119 (**self).read_to_string(path).await
120 }
121
122 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
123 (**self).read_dir(path).await
124 }
125
126 async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
127 (**self).metadata(path).await
128 }
129
130 async fn try_exists(&self, path: &Path) -> io::Result<bool> {
131 (**self).try_exists(path).await
132 }
133}
134
135/// One entry returned by [`ReadStorage::read_dir`].
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct DirEntry {
138 path: PathBuf,
139 file_type: FileType,
140}
141
142impl DirEntry {
143 /// Construct an entry from its path and type.
144 pub fn new(path: impl Into<PathBuf>, file_type: FileType) -> Self {
145 Self {
146 path: path.into(),
147 file_type,
148 }
149 }
150
151 /// The full path to the entry.
152 pub fn path(&self) -> &Path {
153 &self.path
154 }
155
156 /// The final component of the entry's path.
157 pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
158 self.path.file_name()
159 }
160
161 /// The entry's type.
162 pub fn file_type(&self) -> FileType {
163 self.file_type
164 }
165}
166
167/// Metadata about a filesystem entry — the subset prov needs.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct Metadata {
170 file_type: FileType,
171 len: u64,
172 modified: Option<SystemTime>,
173}
174
175impl Metadata {
176 /// Construct metadata from its parts.
177 pub fn new(file_type: FileType, len: u64, modified: Option<SystemTime>) -> Self {
178 Self {
179 file_type,
180 len,
181 modified,
182 }
183 }
184
185 /// The entry's type.
186 pub fn file_type(&self) -> FileType {
187 self.file_type
188 }
189
190 /// Whether the entry is a regular file.
191 pub fn is_file(&self) -> bool {
192 self.file_type.is_file()
193 }
194
195 /// Whether the entry is a directory.
196 pub fn is_dir(&self) -> bool {
197 self.file_type.is_dir()
198 }
199
200 /// Size in bytes.
201 pub fn len(&self) -> u64 {
202 self.len
203 }
204
205 /// Whether the entry is empty.
206 pub fn is_empty(&self) -> bool {
207 self.len == 0
208 }
209
210 /// Last-modified time, if the backend reports one. Mirrors
211 /// [`std::fs::Metadata::modified`], returning [`io::ErrorKind::Unsupported`]
212 /// when unavailable.
213 pub fn modified(&self) -> io::Result<SystemTime> {
214 self.modified
215 .ok_or_else(|| io::Error::new(io::ErrorKind::Unsupported, "modified time unavailable"))
216 }
217}
218
219/// [`ReadStorage`] over the process filesystem (`std::fs`).
220///
221/// Reads only. The matching `Storage` implementation — everything that changes
222/// a byte — is `prov-store`'s, so this adapter is writable exactly when that
223/// crate is in the dependency graph.
224///
225/// The trait is async so that genuinely async backends (network, OPFS) fit;
226/// this adapter's futures are immediately ready, so any executor — including
227/// the dependency-free [`crate::exec::block_on`] — drives them to completion
228/// in a single poll.
229#[derive(Debug, Clone, Copy, Default)]
230pub struct StdFs;
231
232impl ReadStorage for StdFs {
233 async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
234 std::fs::read(path)
235 }
236
237 async fn read_to_string(&self, path: &Path) -> io::Result<String> {
238 std::fs::read_to_string(path)
239 }
240
241 async fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
242 std::fs::read_dir(path)?
243 .map(|entry| {
244 let entry = entry?;
245 Ok(DirEntry::new(
246 entry.path(),
247 convert_file_type(entry.file_type()?),
248 ))
249 })
250 .collect()
251 }
252
253 async fn metadata(&self, path: &Path) -> io::Result<Metadata> {
254 let md = std::fs::metadata(path)?;
255 Ok(Metadata::new(
256 convert_file_type(md.file_type()),
257 md.len(),
258 md.modified().ok(),
259 ))
260 }
261}
262
263fn convert_file_type(ft: std::fs::FileType) -> FileType {
264 if ft.is_dir() {
265 FileType::DIR
266 } else if ft.is_file() {
267 FileType::FILE
268 } else {
269 FileType::SYMLINK
270 }
271}
272
273/// The type of a filesystem entry.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub struct FileType {
276 is_dir: bool,
277 is_file: bool,
278 is_symlink: bool,
279}
280
281impl FileType {
282 /// A regular file.
283 pub const FILE: FileType = FileType {
284 is_dir: false,
285 is_file: true,
286 is_symlink: false,
287 };
288
289 /// A directory.
290 pub const DIR: FileType = FileType {
291 is_dir: true,
292 is_file: false,
293 is_symlink: false,
294 };
295
296 /// A symbolic link.
297 pub const SYMLINK: FileType = FileType {
298 is_dir: false,
299 is_file: false,
300 is_symlink: true,
301 };
302
303 /// Whether this is a regular file.
304 pub fn is_file(&self) -> bool {
305 self.is_file
306 }
307
308 /// Whether this is a directory.
309 pub fn is_dir(&self) -> bool {
310 self.is_dir
311 }
312
313 /// Whether this is a symbolic link.
314 pub fn is_symlink(&self) -> bool {
315 self.is_symlink
316 }
317}