Skip to main content

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}