Skip to main content

vfsi_core/api/
vfsi.rs

1use crate::api::{
2    Attrs, CopyOptions, DirectoryListing, MkDirOp, OpenOp, ResourceLimits, Result, SetAttrsOp,
3    WriteResult,
4};
5use std::path::Path;
6
7use super::{DirHandle, FileHandle};
8
9/// Vectorized filesystem operations for direct and routed application clients.
10/// Construction, builders and protocol-specific diagnostics stay concrete.
11/// Vectors are strict, ordered results on success, not transactions: errors
12/// can follow partially completed requests and never imply rollback.
13/// Generic vector and callback methods require static generic dispatch, not
14/// `dyn Vfsi`. It adds no boxing, data copies or serial-loop fallbacks.
15///
16/// # Choosing an operation
17///
18/// | Task | Start with |
19/// | --- | --- |
20/// | Complete small files | [`vread`](Self::vread), [`write_files`](super::VfsiExt::write_files) |
21/// | Repeated/range I/O on owned handles | [`vopen`](Self::vopen), [`vread`](Self::vread), [`vwrite`](Self::vwrite) |
22/// | Large files without collecting them | [`vstream`](Self::vstream) |
23/// | Directory pages with entry metadata | [`vlistdirs`](Self::vlistdirs) |
24/// | Recursive directory pages | [`vlistdirs`](Self::vlistdirs) with [`ListDirOptions::with_recursive`](crate::api::ListDirOptions::with_recursive) |
25///
26/// Generic application code needs a `Vfsi` bound. Import [`VfsiExt`](super::VfsiExt) for
27/// convenience operations such as `read_files`, `write_files`, and scalar open.
28/// [`VfsiExt::read_dirs_with_options`](super::VfsiExt::read_dirs_with_options) collects directory pages into vectors;
29/// [`VfsiExt::read_stream_with_options`](super::VfsiExt::read_stream_with_options) adapts streaming to a single path.
30/// Use the core vector operations above to submit multiple targets together.
31/// Extension helpers compose vectors; backend-specific execution stays
32/// here so batching, paging, and recovery do not become scalar-loop fallbacks:
33///
34/// ```no_run
35/// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
36/// fn load_parts(fs: &impl Vfsi) -> vfsi_core::api::Result<Vec<Vec<u8>>> {
37///     fs.read_files(&["/file-1", "/file-2"])
38/// }
39/// ```
40///
41/// # Shared semantics
42///
43/// Paths use the client's namespace, not necessarily the host filesystem.
44/// A leading `/` refers to the configured root; this is not security confinement.
45/// Handles in a vector must belong to this client's connection. Successful
46/// vector results match input order; one vector may span several compounds.
47/// Callback cancellation returns a completed/stopped prefix for vector visitors;
48/// directory-page waves may leave several started roots stopped.
49/// A failed mutation can have partial effects. An error's index identifies an
50/// input when known, **not** a committed-prefix count. Preserve the structured
51/// [`crate::api::Error`] rather than blindly replaying a failed write.
52///
53/// Allocation/traversal defaults come from [`limits`](Self::limits). Explicit
54/// per-call options override them; neither is a process-wide memory cap.
55/// Calls are synchronous. Callback methods invoke user code outside the backend
56/// lock and propagate callback errors, but do not provide a filesystem snapshot.
57/// Writes are not automatically durable: use [`Vfsi::vfsync`] on open handles
58/// when required. Drop queues handle cleanup
59/// best-effort; explicit close methods let applications observe cleanup errors.
60pub trait Vfsi {
61    /// Whether object IDs from these paths share a backend/server namespace.
62    /// Single-backend clients use the default. Routers must override it;
63    /// equal numeric IDs from unrelated servers do not identify one object.
64    /// This is route discovery, not an object-equality or existence check.
65    fn same_object_namespace(&self, _left: &Path, _right: &Path) -> bool {
66        true
67    }
68
69    /// Query filesystems for paths (following symlinks) and retained open handles.
70    /// Results preserve input order. Unsupported fields are `None`.
71    fn vstatfs<P: crate::AsTarget<Self::File>>(
72        &self,
73        targets: &[P],
74    ) -> Result<Vec<crate::FilesystemStats>>;
75
76    /// Query paths and retained handles in input order with selected fields.
77    /// Final-symlink behavior applies to paths; handles retain their opened
78    /// identity after rename/unlink. Preflight every handle's ownership and live
79    /// state before dispatching any query. Empty vectors perform no I/O.
80    /// Backend execution must preserve vector batching. Ancestor symlinks use
81    /// ordinary namespace resolution; this is not a snapshot or confinement API.
82    ///
83    /// ```no_run
84    /// use vfsi_core::api::{Vfsi, VfsiExt, AttrsOptions, Attributes, WriteOp};
85    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
86    /// let entries = fs.vgetattrs(&["/file-1", "/link"],
87    ///     AttrsOptions::new().with_fields(Attributes::MODE | Attributes::SIZE)
88    ///         .with_follow_final_symlink(false))?;
89    /// # let _ = entries;
90    /// # Ok(())
91    /// # }
92    /// ```
93    fn vgetattrs<P: crate::AsTarget<Self::File>>(
94        &self,
95        paths: &[P],
96        options: crate::api::AttrsOptions,
97    ) -> Result<Vec<Attrs>>;
98
99    /// Synchronize retained handles in input order. All handles must belong to
100    /// this client and be live; validate the entire vector before dispatch.
101    /// Errors may follow completed synchronization of earlier handles. Empty
102    /// vectors succeed without I/O; ambiguous failures must not be replayed.
103    fn vfsync(&self, files: &[&Self::File], mode: crate::api::SyncMode) -> Result<()>;
104
105    /// Update selected attributes for paths or open handles using native batching.
106    /// Every handle is validated before dispatch, including ownership and closure.
107    /// Unspecified fields are unchanged. Each [`SetAttrsOp`] controls whether
108    /// its final path symlink is followed; this does not change open-handle identity.
109    /// Ancestor symlinks retain ordinary backend resolution.
110    /// Equal-policy runs remain batched; mixed policies retain input order.
111    /// Validate every input before any run mutates the filesystem.
112    /// Failure can follow partial mutations, including within one request;
113    /// an error index identifies an input, not a committed-prefix count.
114    /// Empty vectors succeed without I/O. Do not replay ambiguous failures.
115    ///
116    /// ```no_run
117    /// use vfsi_core::api::{Vfsi, Permissions, SetAttrsOp};
118    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
119    /// fs.vsetattrs(&[
120    ///     SetAttrsOp::new("/file-1").permissions(Permissions::from_mode(0o640)).len(1024),
121    ///     SetAttrsOp::new("/file-2").len(0),
122    /// ])?;
123    /// # Ok(())
124    /// # }
125    /// ```
126    /// Handle targets and paths can share a batch:
127    ///
128    /// ```no_run
129    /// use vfsi_core::api::{Vfsi, Target, SetAttrsOp};
130    /// # fn example<F: Vfsi>(fs: &F, file: &F::File) -> vfsi_core::api::Result<()> {
131    /// fs.vsetattrs(&[
132    ///     SetAttrsOp::file(file).len(1024),
133    ///     SetAttrsOp::new(Target::Path(std::path::Path::new("/other"))).len(0)
134    ///         .with_follow_final_symlink(false),
135    /// ])?;
136    /// # Ok(())
137    /// # }
138    /// ```
139    fn vsetattrs<P: crate::api::AsTarget<Self::File>>(
140        &self,
141        updates: &[SetAttrsOp<P>],
142    ) -> Result<()>;
143
144    /// Capabilities supported by this client. Routed clients report capabilities
145    /// common to their routes; support does not imply authorization for a path.
146    fn capabilities(&self) -> Result<crate::api::Capabilities>;
147
148    /// Create symbolic links from `(target text, link path)` pairs.
149    /// Targets are stored verbatim, including relative or dangling targets.
150    /// Parents must exist. Errors may follow partial creation; no rollback.
151    fn vsymlink<P: AsRef<Path>, Q: AsRef<Path>>(&self, pairs: &[(P, Q)]) -> Result<()>;
152
153    /// Read symbolic link targets in input order without following final links.
154    /// Returns their original path bytes, including non-UTF-8 names on Unix.
155    /// An error identifies the input when known and discards collected results.
156    fn vreadlink<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<std::path::PathBuf>>;
157
158    /// Create hard links from `(existing source, new link path)` pairs.
159    /// Sources are not followed when the final component is a symlink.
160    /// Cross-filesystem links can fail. Errors may follow partial creation.
161    fn vhardlink<P: AsRef<Path>, Q: AsRef<Path>>(&self, pairs: &[(P, Q)]) -> Result<()>;
162
163    /// Owned directory handle for this client's retained namespace objects.
164    type Dir: DirHandle + 'static;
165
166    /// Open genuine directory handles in input order without following final
167    /// symlinks. Path-only backends must report unsupported. On error, opened
168    /// handles are cleaned up; an empty vector performs no I/O.
169    fn vopen_dirs<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<Self::Dir>> {
170        if paths.is_empty() {
171            Ok(Vec::new())
172        } else {
173            Err(crate::api::Error::unsupported(0))
174        }
175    }
176
177    /// Remove contents anchored to retained directories, preserving the roots.
178    /// Validate every handle's ownership and live state before any mutation.
179    /// Never reopen diagnostic paths. Errors can follow partial mutation; an
180    /// index identifies an input, not a committed prefix. Do not replay failures.
181    fn vremove_dir_contents(
182        &self,
183        dirs: &[&Self::Dir],
184        options: crate::RemoveOptions,
185    ) -> Result<()> {
186        let _ = options;
187        if dirs.is_empty() {
188            Ok(())
189        } else {
190            Err(crate::api::Error::unsupported(0))
191        }
192    }
193
194    /// Owned handle; vectors must contain handles belonging to this client.
195    type File: FileHandle + 'static;
196
197    /// Collection/batch defaults, not a process memory cap or file-reader cap.
198    ///
199    /// This only inspects policy; it performs no filesystem I/O. Configure a
200    /// concrete client's builder or `with_limits` to change its defaults.
201    ///
202    /// ```no_run
203    /// use vfsi_core::api::Vfsi;
204    /// # fn example(fs: &impl Vfsi) {
205    /// let limits = fs.limits();
206    /// println!("owned-read budget: {} bytes", limits.max_read_bytes());
207    /// println!("walk depth: {}", limits.max_walk_depth().get());
208    /// # }
209    /// ```
210    fn limits(&self) -> ResourceLimits;
211    /// Strict ordered OPEN results. Failure releases returned handles but cannot
212    /// undo files created/truncated earlier; `index()` is not a progress count.
213    ///
214    /// Different requests can use different flags/modes. All resulting handles
215    /// belong to this client. Prefer this to a scalar loop when opening a cohort.
216    /// Singleton vectors must preserve scalar symlink resolution and independently
217    /// opened handle state: VfsiExt::open_with is built from this primitive.
218    ///
219    /// ```no_run
220    /// use vfsi_core::api::{Vfsi, VfsiExt, OpenFlags, OpenOp, WriteOp};
221    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
222    /// let files = fs.vopen(&[
223    ///     OpenOp::new("/file-1", OpenFlags::READ),
224    ///     OpenOp::new("/file-2", OpenFlags::READ),
225    /// ])?;
226    /// // files[0] corresponds to file-1; files[1] to file-2.
227    /// fs.close_files(files)?;
228    /// # Ok(())
229    /// # }
230    /// ```
231    fn vopen(&self, requests: &[OpenOp]) -> Result<Vec<Self::File>>;
232    /// Consume a read batch with an explicit aggregate logical-byte budget.
233    /// Large files should usually be streamed instead of increasing the budget.
234    /// Explicit options override client defaults.
235    ///
236    /// ```no_run
237    /// use vfsi_core::api::{Vfsi, VfsiExt, ReadOp, ReadOptions, WriteOp};
238    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
239    /// let results = fs.vread(
240    ///     [ReadOp::whole("/config")],
241    ///     ReadOptions::new().with_max_total_bytes(std::num::NonZeroUsize::new(1024 * 1024).unwrap()),
242    /// )?;
243    /// println!("{} bytes", results[0].bytes_read());
244    /// # Ok(())
245    /// # }
246    /// ```
247    ///
248    /// Consume whole-file, allocating-range, and caller-buffer operations.
249    ///
250    /// Construction does no I/O. Results retain input order and never borrow
251    /// caller storage. Whole files complete or fail; ranges may return short
252    /// progress. Only `buffer[..result.bytes_read()]` is valid after a buffered read.
253    /// The default aggregate budget comes from `limits().max_read_bytes()`.
254    /// All range and buffer lengths must fit before dispatch. Buffer reads run
255    /// first, then allocating ranges, then whole paths, in batched phases.
256    /// Errors retain original indices; buffers may already contain partial
257    /// progress on failure. No snapshot or atomicity is promised.
258    ///
259    /// ```no_run
260    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, ReadOp, WriteOp};
261    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
262    /// let file = fs.open("/large")?;
263    /// let mut buffer = [0_u8; 4096];
264    /// let result = fs.vread([
265    ///     ReadOp::whole("/config"),
266    ///     ReadOp::range(&file, 1024, 4096),
267    ///     ReadOp::into(&file, 8192, &mut buffer),
268    /// ], Default::default());
269    /// let close = file.close();
270    /// let results = result?;
271    /// close?;
272    /// println!("config: {:?}", results[0].data().unwrap());
273    /// println!("buffer: {:?}", &buffer[..results[2].bytes_read()]);
274    /// # Ok(())
275    /// # }
276    /// ```
277    fn vread<'a>(
278        &self,
279        ops: impl IntoIterator<Item = crate::api::ReadOp<'a, Self::File>>,
280        options: crate::api::ReadOptions,
281    ) -> Result<Vec<crate::api::ReadResult>>;
282    /// Write positional ranges with explicit short-write policy.
283    /// With default options, report accepted byte counts without completing short writes.
284    /// With `write_all(true)`, finish short writes after whole-batch local preflight.
285    /// Backend failures can still follow mutations; aliasing paths are the
286    /// caller's responsibility and do not imply transactional ordering.
287    ///
288    /// Handles opened for append select EOF regardless of the requested offset.
289    /// With completion enabled, success means every request's payload was written.
290    /// Short writes are completed at their remaining offsets; this does not
291    /// authorize replay after an ambiguous failure or guarantee durability.
292    ///
293    /// ```no_run
294    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenOp, WriteOp};
295    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
296    /// let files = fs.vopen(&[
297    ///     OpenOp::new("/file-1", OpenFlags::WRITE),
298    ///     OpenOp::new("/file-2", OpenFlags::WRITE),
299    /// ])?;
300    /// let result = fs.vwrite(&[
301    ///     WriteOp::at(&files[0], 0, b"hello"),
302    ///     WriteOp::at(&files[1], 4096, b"world"),
303    /// ], vfsi_core::api::WriteOptions::new().with_write_all(true));
304    /// let close = fs.close_files(files);
305    /// result?;
306    /// close?;
307    /// # Ok(())
308    /// # }
309    /// ```
310    fn vwrite<'a>(
311        &self,
312        requests: &[crate::api::WriteOp<'a, Self::File>],
313        options: crate::api::WriteOptions,
314    ) -> Result<Vec<WriteResult>>;
315    /// Retain handles for failed cleanup; completed groups can already be
316    /// closed. A lost reply leaves remote state ambiguous, not safely open.
317    ///
318    /// Unlike [`close_files`](super::VfsiExt::close_files), this borrows the handles. After an error,
319    /// `is_closed()` reports local cleanup ownership only; reconcile/close the
320    /// retained handles rather than resuming ordinary I/O as if nothing happened.
321    ///
322    /// ```no_run
323    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenOp, WriteOp};
324    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
325    /// let mut files = fs.vopen(&[OpenOp::new("/file-1", OpenFlags::READ)])?;
326    /// if let Err(error) = fs.vclose(&mut files) {
327    ///     let retained = files.iter().filter(|file| !file.is_closed()).count();
328    ///     eprintln!("{retained} handles retain cleanup ownership: {error}");
329    ///     return Err(error); // Drop performs best-effort cleanup, not reconciliation.
330    /// }
331    /// # Ok(())
332    /// # }
333    /// ```
334    fn vclose(&self, files: &mut [Self::File]) -> Result<()>;
335    /// Strict vector directory creation. Parents must already exist; errors
336    /// can follow completed mutations, and do not imply rollback.
337    ///
338    /// Submit independent siblings together. Do not rely on creating a parent
339    /// and its child in the same vector; create parents first or use
340    /// a dependency-aware tree builder for dependency-aware fresh-tree creation.
341    ///
342    /// ```no_run
343    /// use vfsi_core::api::{Vfsi, VfsiExt, MkDirOp};
344    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
345    /// fs.create_dir_all("/workspace")?;
346    /// fs.vmkdir(&[MkDirOp::new("/workspace/input", 0o750), MkDirOp::new("/workspace/output", 0o700)])?;
347    /// # Ok(())
348    /// # }
349    /// ```
350    /// Each [`MkDirOp`] supplies a path and Unix permission bits; backends apply the
351    /// requested mode rather than relying on the process umask. Unsupported
352    /// permission semantics are reported by the backend.
353    fn vmkdir<P: AsRef<Path>>(&self, directories: &[MkDirOp<P>]) -> Result<()>;
354    /// Strict file-copy batches; a failed call can have copied earlier files.
355    ///
356    /// Pairs are `(source, destination)` in this client's namespace. Contents
357    /// are copied, not a recursive tree or a metadata-preserving snapshot.
358    /// `options` controls whether final source symlinks are followed; ancestor
359    /// symlinks retain ordinary path-resolution behavior.
360    /// Avoid aliasing sources/destinations; batching does not make overlapping
361    /// copies transactional or safe to replay after ambiguous failure.
362    ///
363    /// ```no_run
364    /// use vfsi_core::api::{CopyOptions, Vfsi, VfsiExt, WriteOp};
365    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
366    /// fs.vcopy(&[
367    ///     ("/input/file-1", "/output/file-1"),
368    ///     ("/input/file-2", "/output/file-2"),
369    /// ], CopyOptions::default())?;
370    /// # Ok(())
371    /// # }
372    /// ```
373    fn vcopy<P: AsRef<Path>, Q: AsRef<Path>>(
374        &self,
375        pairs: &[(P, Q)],
376        options: CopyOptions,
377    ) -> Result<()>;
378    /// Remove entries, recursive trees, or directory contents with explicit policy.
379    ///
380    /// Contents mode retains each root and uses native anchored removal without
381    /// following final symlinks. Failures may follow partial mutations; no mode
382    /// provides rollback. Options control bounded retries and error handling.
383    ///
384    /// ```no_run
385    /// use vfsi_core::api::{Vfsi, RemoveMode, RemoveOptions};
386    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
387    /// fs.vremove(&["/scratch-1", "/scratch-2"],
388    ///     RemoveMode::Contents, RemoveOptions::new())?;
389    /// # Ok(())
390    /// # }
391    /// ```
392    fn vremove<P: AsRef<Path>>(
393        &self,
394        paths: &[P],
395        mode: crate::api::RemoveMode,
396        options: crate::api::RemoveOptions,
397    ) -> Result<()>;
398    /// Rename independent pairs in input order using the requested atomic
399    /// destination behavior. Each pair is atomic where the backend supports
400    /// the option; the vector is not a transaction, so failures may leave a
401    /// completed prefix. Source and destination are resolved in this filesystem
402    /// namespace. Unsupported guarantees return `Unsupported` rather than
403    /// being emulated with check-then-rename.
404    ///
405    /// ```no_run
406    /// use vfsi_core::api::{RenameMode, Vfsi};
407    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
408    /// fs.vrename(
409    ///     &[("/old-1", "/new-1"), ("/old-2", "/new-2")],
410    ///     Default::default(), // ordinary replacement semantics
411    /// )?;
412    /// # Ok(())
413    /// # }
414    /// ```
415    fn vrename<P: AsRef<Path>, Q: AsRef<Path>>(
416        &self,
417        pairs: &[(P, Q)],
418        options: crate::api::RenameMode,
419    ) -> Result<()>;
420
421    /// Visit shallow directories (default) or recursive trees using bounded pages.
422    /// Entry/path-byte limits are shared across roots; recursive visits also
423    /// charge retained directory path bytes. Root index accompanies every owned directory page, including empty directories.
424    /// Callbacks run outside backend locks and may reenter. Entries for one directory may span several pages. Break stops the
425    /// whole vector. The returned prefix covers every root whose pages reached
426    /// the callback; unfinished roots are Stopped. Pages may interleave between roots.
427    ///
428    /// Sorting and enter/leave options are rejected before I/O; use VfsiExt::listdir
429    /// for those policies.
430    /// Metadata is selected on directory pages, not through per-entry stat
431    /// calls. Entry symlinks are never followed during recursive descent.
432    /// Descendants reopened by pathname reject symlinks in every component;
433    /// backends unable to guarantee this return Unsupported. Input roots retain
434    /// the supplied path-resolution policy.
435    /// Each page owns its entries; collection can transfer them without cloning.
436    /// Traversal order is backend-defined; this does not provide a snapshot.
437    ///
438    /// ```no_run
439    /// use vfsi_core::api::{Vfsi, ListDirOptions, ControlFlow};
440    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
441    /// fs.vlistdirs(&["/tree-1", "/tree-2"],
442    ///     ListDirOptions::new().with_recursive(true).with_max_depth(vfsi_core::api::DepthLimit::new(8).expect("finite depth")),
443    ///     |index, page| {
444    ///         println!("{index}: {} ({} entries)", page.path.display(), page.entries.len());
445    ///         Ok(ControlFlow::Continue(()))
446    ///     })?;
447    /// # Ok(())
448    /// # }
449    /// ```
450    fn vlistdirs<P: AsRef<Path>>(
451        &self,
452        paths: &[P],
453        options: crate::api::ListDirOptions,
454        callback: impl FnMut(usize, DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
455    ) -> Result<Vec<crate::api::TraversalCompletion>>;
456
457    /// Stream files through bounded chunks without collecting whole contents.
458    /// The callback receives the input index, byte offset, and borrowed chunk.
459    /// `ControlFlow::Break(())` stops the whole vector; results contain its completed/stopped prefix.
460    /// Backends may process streams sequentially; this is not a parallelism promise.
461    ///
462    /// ```no_run
463    /// use vfsi_core::api::{Vfsi, StreamOptions};
464    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
465    /// fs.vstream(&["/large-1", "/large-2"],
466    ///     StreamOptions::new().with_chunk_size(std::num::NonZeroUsize::new(1024 * 1024).unwrap()), |index, offset, data| {
467    ///         println!("{index}: {} bytes at {offset}", data.len());
468    ///         Ok(std::ops::ControlFlow::Continue(()))
469    ///     })?;
470    /// # Ok(())
471    /// # }
472    /// ```
473    fn vstream<P: AsRef<Path>>(
474        &self,
475        paths: &[P],
476        options: crate::api::StreamOptions,
477        callback: impl FnMut(usize, u64, &[u8]) -> Result<std::ops::ControlFlow<()>>,
478    ) -> Result<Vec<crate::api::StreamCompletion>>;
479}