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}