vfsi_core/api/traits.rs
1//! Protocol-independent application contracts. Backend implementer traits
2//! live in implementation crates; generic applications need only these traits.
3
4use crate::api::{DirectoryListing, Metadata, OpenRequest, ResourceLimits, Result, WriteResult};
5use std::io::{Read, Seek, SeekFrom, Write};
6use std::path::Path;
7
8/// Owned application file. Borrowed requests preserve the file's lifetime;
9/// clients validate connection ownership before dispatching a vector.
10///
11/// External implementations must keep request storage valid for its borrowed
12/// lifetime, validate client ownership before I/O, and report positional progress
13/// without changing the cursor. Request constructors do no I/O. Associated
14/// request types are implementer contracts, not application-facing builders.
15/// A backend must retain ownership of live descriptors through cleanup failures.
16pub trait FileHandle: Read + Write + Seek {
17 /// Borrowed positional read request; constructing it performs no I/O.
18 type ReadRequest<'a>
19 where
20 Self: 'a;
21 /// Borrowed positional request into a caller-owned destination buffer.
22 type ReadIntoRequest<'a>
23 where
24 Self: 'a;
25 /// Diagnostic name captured at open; not current path or object identity.
26 fn path(&self) -> &Path;
27 /// Query the opened object, even if its original pathname was renamed.
28 fn metadata(&self) -> Result<Metadata>;
29 /// Prepare a non-cursor-changing read for this handle's owning client.
30 fn read_request_at(&self, offset: u64, length: usize) -> Self::ReadRequest<'_>;
31 /// Borrow both the handle and destination until the vector call completes.
32 fn read_request_at_into<'a>(
33 &'a self,
34 offset: u64,
35 buffer: &'a mut [u8],
36 ) -> Self::ReadIntoRequest<'a>;
37 /// Read up to the buffer's length without changing the cursor; may be short.
38 fn read_at(&self, buffer: &mut [u8], offset: u64) -> Result<usize>;
39 /// Write up to the input's length without changing the cursor; may be short.
40 fn write_at(&self, buffer: &[u8], offset: u64) -> Result<usize>;
41 /// Cursor-based read retaining structured errors, unlike the `Read` adapter.
42 fn read_native(&mut self, buffer: &mut [u8]) -> Result<usize>;
43 /// Collect from the current cursor with a caller-selected payload budget.
44 /// A failed read may advance the cursor; exceeding the limit is an error,
45 /// not successful truncation. Standard `Read::read_to_end` is not bounded.
46 fn read_to_end_with_limit(&mut self, max_bytes: usize) -> Result<Vec<u8>>;
47 /// Cursor-based possibly short write retaining structured errors.
48 fn write_native(&mut self, buffer: &[u8]) -> Result<usize>;
49 /// Change the cursor, retaining structured errors unlike `Seek`.
50 fn seek_native(&mut self, position: SeekFrom) -> Result<u64>;
51 /// Request durability of file data; errors are not discarded.
52 fn sync_data(&self) -> Result<()>;
53 /// Request durability of file data and metadata supported by the backend.
54 fn sync_all(&self) -> Result<()>;
55 /// Close without surrendering cleanup ownership on failure. After an
56 /// ambiguous failure, reconcile/close rather than resume ordinary I/O.
57 fn try_close(&mut self) -> Result<()>;
58 /// Local close ownership only, not proof of remote liveness after failure.
59 fn is_closed(&self) -> bool;
60 /// Consuming close: on failure the handle is lost and Drop retries cleanup
61 /// best-effort on a later operation or cleanup drain. Prefer `try_close` when
62 /// cleanup failures need reconciliation.
63 fn close(self) -> Result<()>
64 where
65 Self: Sized;
66}
67
68/// Vectorized filesystem operations for direct and routed application clients.
69/// Construction, builders and protocol-specific diagnostics stay concrete.
70/// Vectors are strict, ordered results on success, not transactions: errors
71/// can follow partially completed requests and never imply rollback.
72/// This uses borrowed request GATs, so it is for static generic dispatch, not
73/// `dyn Vfsi`. It adds no boxing, data copies or serial-loop fallbacks.
74///
75/// # Choosing an operation
76///
77/// | Task | Start with |
78/// | --- | --- |
79/// | Complete small files | [`vread`](Self::vread), [`write_files`](VfsiExt::write_files) |
80/// | Repeated/range I/O on owned handles | [`vopen`](Self::vopen), [`vread`](Self::vread), [`vwrite`](Self::vwrite) |
81/// | Large files without collecting them | [`vstream`](Self::vstream) |
82/// | Directory pages with entry metadata | [`vlistdirs`](Self::vlistdirs) |
83/// | Recursive directory pages | [`vlistdirs`](Self::vlistdirs) with [`VisitOptions::recursive`](crate::api::VisitOptions::recursive) |
84///
85/// Generic application code needs an `Vfsi` bound. Import [`VfsiExt`] for
86/// convenience operations such as `read_files`, `write_files`, and scalar open.
87/// [`VfsiExt::read_dirs_with_options`] collects directory pages into vectors;
88/// [`VfsiExt::read_stream_with_options`] adapts streaming to a single path.
89/// Use the core vector operations above to submit multiple targets together.
90/// Extension helpers compose vectors; backend-specific execution stays
91/// here so batching, paging, and recovery do not become scalar-loop fallbacks:
92///
93/// ```no_run
94/// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
95/// fn load_parts(fs: &impl Vfsi) -> vfsi_core::api::Result<Vec<Vec<u8>>> {
96/// fs.read_files(&["/file-1", "/file-2"])
97/// }
98/// ```
99///
100/// # Shared semantics
101///
102/// Paths use the client's namespace, not necessarily the host filesystem.
103/// A leading `/` refers to the configured root; this is not security confinement.
104/// Handles in a vector must belong to this client's connection. Successful
105/// vector results match input order; one vector may span several compounds.
106/// Callback cancellation returns a completed/stopped prefix for vector visitors;
107/// directory-page waves may leave several started roots stopped.
108/// A failed mutation can have partial effects. An error's index identifies an
109/// input when known, **not** a committed-prefix count. Preserve the structured
110/// [`crate::api::Error`] rather than blindly replaying a failed write.
111///
112/// Allocation/traversal defaults come from [`limits`](Self::limits). Explicit
113/// per-call options override them; neither is a process-wide memory cap.
114/// Calls are synchronous. Callback methods invoke user code outside the backend
115/// lock and propagate callback errors, but do not provide a filesystem snapshot.
116/// Writes are not automatically durable: use [`FileHandle::sync_data`] or
117/// [`FileHandle::sync_all`] on an open handle when required. Drop queues handle cleanup
118/// best-effort; explicit close methods let applications observe cleanup errors.
119pub trait Vfsi {
120 /// Query metadata in input order with selected fields and final-symlink behavior.
121 /// Backend execution must preserve vector batching. Ancestor symlinks use
122 /// ordinary namespace resolution; this is not a snapshot or confinement API.
123 ///
124 /// ```no_run
125 /// use vfsi_core::api::{Vfsi, VfsiExt, MetadataOptions, MetadataFields, WriteOp};
126 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
127 /// let entries = fs.vgetattrs(&["/file-1", "/link"],
128 /// MetadataOptions::new().fields(MetadataFields::MODE | MetadataFields::SIZE)
129 /// .follow_symlinks(false))?;
130 /// # let _ = entries;
131 /// # Ok(())
132 /// # }
133 /// ```
134 fn vgetattrs<P: AsRef<Path>>(
135 &self,
136 paths: &[P],
137 options: crate::api::MetadataOptions,
138 ) -> Result<Vec<Metadata>>;
139
140 /// Owned handle; vectors must contain handles belonging to this client.
141 type File: FileHandle + 'static;
142
143 /// Collection/batch defaults, not a process memory cap or file-reader cap.
144 ///
145 /// This only inspects policy; it performs no filesystem I/O. Configure a
146 /// concrete client's builder or `with_limits` to change its defaults.
147 ///
148 /// ```no_run
149 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
150 /// # fn example(fs: &impl Vfsi) {
151 /// let limits = fs.limits();
152 /// println!("owned-read budget: {} bytes", limits.max_read_bytes);
153 /// println!("walk depth: {}", limits.max_walk_depth);
154 /// # }
155 /// ```
156 fn limits(&self) -> ResourceLimits;
157 /// Strict ordered OPEN results. Failure releases returned handles but cannot
158 /// undo files created/truncated earlier; `index()` is not a progress count.
159 ///
160 /// Different requests can use different flags/modes. All resulting handles
161 /// belong to this client. Prefer this to a scalar loop when opening a cohort.
162 /// Singleton vectors must preserve scalar symlink resolution and independently
163 /// opened handle state: VfsiExt::open_with is built from this primitive.
164 ///
165 /// ```no_run
166 /// use vfsi_core::api::{Vfsi, VfsiExt, OpenFlags, OpenRequest, WriteOp};
167 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
168 /// let files = fs.vopen(&[
169 /// OpenRequest::new("/file-1", OpenFlags::READ),
170 /// OpenRequest::new("/file-2", OpenFlags::READ),
171 /// ])?;
172 /// // files[0] corresponds to file-1; files[1] to file-2.
173 /// fs.close_files(files)?;
174 /// # Ok(())
175 /// # }
176 /// ```
177 fn vopen(&self, requests: &[OpenRequest]) -> Result<Vec<Self::File>>;
178 /// Consume a read batch with an explicit aggregate logical-byte budget.
179 /// Large files should usually be streamed instead of increasing the budget.
180 /// Explicit options override client defaults.
181 ///
182 /// ```no_run
183 /// use vfsi_core::api::{Vfsi, VfsiExt, ReadOp, ReadOptions, WriteOp};
184 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
185 /// let results = fs.vread(
186 /// [ReadOp::whole("/config")],
187 /// ReadOptions::new().max_total_bytes(std::num::NonZeroUsize::new(1024 * 1024)),
188 /// )?;
189 /// println!("{} bytes", results[0].read());
190 /// # Ok(())
191 /// # }
192 /// ```
193 ///
194 /// Consume whole-file, allocating-range, and caller-buffer operations.
195 ///
196 /// Construction does no I/O. Results retain input order and never borrow
197 /// caller storage. Whole files complete or fail; ranges may return short
198 /// progress. Only `buffer[..result.read]` is valid after a buffered read.
199 /// The default aggregate budget comes from `limits().max_read_bytes`.
200 /// All range and buffer lengths must fit before dispatch. Buffer reads run
201 /// first, then allocating ranges, then whole paths, in batched phases.
202 /// Errors retain original indices; buffers may already contain partial
203 /// progress on failure. No snapshot or atomicity is promised.
204 ///
205 /// ```no_run
206 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, ReadOp, WriteOp};
207 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
208 /// let file = fs.open("/large")?;
209 /// let mut buffer = [0_u8; 4096];
210 /// let result = fs.vread([
211 /// ReadOp::whole("/config"),
212 /// ReadOp::range(&file, 1024, 4096),
213 /// ReadOp::into(&file, 8192, &mut buffer),
214 /// ], Default::default());
215 /// let close = file.close();
216 /// let results = result?;
217 /// close?;
218 /// println!("config: {:?}", results[0].data().unwrap());
219 /// println!("buffer: {:?}", &buffer[..results[2].read()]);
220 /// # Ok(())
221 /// # }
222 /// ```
223 fn vread<'a>(
224 &self,
225 ops: impl IntoIterator<Item = crate::api::ReadOp<'a, Self::File>>,
226 options: crate::api::ReadOptions,
227 ) -> Result<Vec<crate::api::ReadResult>>;
228 /// Write positional ranges with explicit short-write policy.
229 /// With default options, report accepted byte counts without completing short writes.
230 /// With `write_all(true)`, finish short writes after whole-batch local preflight.
231 /// Backend failures can still follow mutations; aliasing paths are the
232 /// caller's responsibility and do not imply transactional ordering.
233 ///
234 /// With completion enabled, success means every request's payload was written.
235 /// Short writes are completed at their remaining offsets; this does not
236 /// authorize replay after an ambiguous failure or guarantee durability.
237 ///
238 /// ```no_run
239 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenRequest, WriteOp};
240 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
241 /// let files = fs.vopen(&[
242 /// OpenRequest::new("/file-1", OpenFlags::WRITE),
243 /// OpenRequest::new("/file-2", OpenFlags::WRITE),
244 /// ])?;
245 /// let result = fs.vwrite(&[
246 /// WriteOp::at(&files[0], 0, b"hello"),
247 /// WriteOp::at(&files[1], 4096, b"world"),
248 /// ], vfsi_core::api::WriteOptions::new().write_all(true));
249 /// let close = fs.close_files(files);
250 /// result?;
251 /// close?;
252 /// # Ok(())
253 /// # }
254 /// ```
255 fn vwrite<'a>(
256 &self,
257 requests: &[crate::api::WriteOp<'a, Self::File>],
258 options: crate::api::WriteOptions,
259 ) -> Result<Vec<WriteResult>>;
260 /// Retain handles for failed cleanup; completed groups can already be
261 /// closed. A lost reply leaves remote state ambiguous, not safely open.
262 ///
263 /// Unlike [`close_files`](VfsiExt::close_files), this borrows the handles. After an error,
264 /// `is_closed()` reports local cleanup ownership only; reconcile/close the
265 /// retained handles rather than resuming ordinary I/O as if nothing happened.
266 ///
267 /// ```no_run
268 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenRequest, WriteOp};
269 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
270 /// let mut files = fs.vopen(&[OpenRequest::new("/file-1", OpenFlags::READ)])?;
271 /// if let Err(error) = fs.vclose(&mut files) {
272 /// let retained = files.iter().filter(|file| !file.is_closed()).count();
273 /// eprintln!("{retained} handles retain cleanup ownership: {error}");
274 /// return Err(error); // Drop performs best-effort cleanup, not reconciliation.
275 /// }
276 /// # Ok(())
277 /// # }
278 /// ```
279 fn vclose(&self, files: &mut [Self::File]) -> Result<()>;
280 /// Strict vector directory creation. Parents must already exist; errors
281 /// can follow completed mutations, and do not imply rollback.
282 ///
283 /// Submit independent siblings together. Do not rely on creating a parent
284 /// and its child in the same vector; create parents first or use
285 /// a dependency-aware tree builder for dependency-aware fresh-tree creation.
286 ///
287 /// ```no_run
288 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
289 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
290 /// fs.create_dir_all("/workspace")?;
291 /// fs.vmkdir(&["/workspace/input", "/workspace/output"])?;
292 /// # Ok(())
293 /// # }
294 /// ```
295 fn vmkdir<P: AsRef<Path>>(&self, paths: &[P]) -> Result<()>;
296 /// Strict file-copy batches; a failed call can have copied earlier files.
297 ///
298 /// Pairs are `(source, destination)` in this client's namespace. Contents
299 /// are copied, not a recursive tree or a metadata-preserving snapshot.
300 /// Avoid aliasing sources/destinations; batching does not make overlapping
301 /// copies transactional or safe to replay after ambiguous failure.
302 ///
303 /// ```no_run
304 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
305 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
306 /// fs.vcopy(&[
307 /// ("/input/file-1", "/output/file-1"),
308 /// ("/input/file-2", "/output/file-2"),
309 /// ])?;
310 /// # Ok(())
311 /// # }
312 /// ```
313 fn vcopy<P: AsRef<Path>, Q: AsRef<Path>>(&self, pairs: &[(P, Q)]) -> Result<()>;
314 /// Remove entries, recursive trees, or directory contents with explicit policy.
315 ///
316 /// Contents mode retains each root and uses native anchored removal without
317 /// following final symlinks. Failures may follow partial mutations; no mode
318 /// provides rollback. Options control bounded retries and error handling.
319 ///
320 /// ```no_run
321 /// use vfsi_core::api::{Vfsi, RemoveMode, RemoveOptions};
322 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
323 /// fs.vremove(&["/scratch-1", "/scratch-2"],
324 /// RemoveMode::Contents, RemoveOptions::new())?;
325 /// # Ok(())
326 /// # }
327 /// ```
328 fn vremove<P: AsRef<Path>>(
329 &self,
330 paths: &[P],
331 mode: crate::api::RemoveMode,
332 options: crate::api::RemoveOptions,
333 ) -> Result<()>;
334 /// Rename independent pairs in input order. Failures may leave a completed prefix.
335 /// Source and destination belong to this filesystem's namespace.
336 ///
337 /// ```no_run
338 /// use vfsi_core::api::Vfsi;
339 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
340 /// fs.vrename(&[("/old-1", "/new-1"), ("/old-2", "/new-2")])?;
341 /// # Ok(())
342 /// # }
343 /// ```
344 fn vrename<P: AsRef<Path>, Q: AsRef<Path>>(&self, pairs: &[(P, Q)]) -> Result<()>;
345
346 /// Visit shallow directories (default) or recursive trees using bounded pages.
347 /// Entry/path-byte limits are shared across roots; recursive visits also
348 /// charge retained directory path bytes. Root index accompanies every owned directory page, including empty directories.
349 /// Callbacks run outside backend locks and may reenter. Entries for one directory may span several pages. Break stops the
350 /// whole vector. The returned prefix covers every root whose pages reached
351 /// the callback; unfinished roots are Stopped. Pages may interleave between roots.
352 ///
353 /// Metadata is selected on directory pages, not through per-entry stat
354 /// calls. Entry symlinks are never followed during recursive descent.
355 /// Each page owns its entries; collection can transfer them without cloning.
356 /// Traversal order is backend-defined; this does not provide a snapshot.
357 ///
358 /// ```no_run
359 /// use vfsi_core::api::{Vfsi, VisitOptions, ControlFlow};
360 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
361 /// fs.vlistdirs(&["/tree-1", "/tree-2"],
362 /// VisitOptions::new().recursive(true).max_depth(8),
363 /// |index, page| {
364 /// println!("{index}: {} ({} entries)", page.path.display(), page.entries.len());
365 /// Ok(ControlFlow::Continue(()))
366 /// })?;
367 /// # Ok(())
368 /// # }
369 /// ```
370 fn vlistdirs<P: AsRef<Path>>(
371 &self,
372 paths: &[P],
373 options: crate::api::VisitOptions,
374 callback: impl FnMut(usize, DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
375 ) -> Result<Vec<crate::api::TraversalCompletion>>;
376
377 /// Stream files through bounded chunks without collecting whole contents.
378 /// The callback receives the input index, byte offset, and borrowed chunk.
379 /// False stops the whole vector; results contain its completed/stopped prefix.
380 /// Backends may process streams sequentially; this is not a parallelism promise.
381 ///
382 /// ```no_run
383 /// use vfsi_core::api::{Vfsi, ReadStreamOptions};
384 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
385 /// fs.vstream(&["/large-1", "/large-2"],
386 /// ReadStreamOptions::new().chunk_size(1024 * 1024), |index, offset, data| {
387 /// println!("{index}: {} bytes at {offset}", data.len());
388 /// Ok(true)
389 /// })?;
390 /// # Ok(())
391 /// # }
392 /// ```
393 fn vstream<P: AsRef<Path>>(
394 &self,
395 paths: &[P],
396 options: crate::api::ReadStreamOptions,
397 callback: impl FnMut(usize, u64, &[u8]) -> Result<bool>,
398 ) -> Result<Vec<crate::api::StreamCompletion>>;
399}
400
401/// Scalar operations and convenience workflows alongside the [`Vfsi`] vectors.
402///
403/// Blanket-implemented for every filesystem implementing Vfsi. Import `VfsiExt` (or
404/// [`crate::api::prelude`]) to use these helpers; generic code needs only an
405/// `Vfsi` bound. Helpers preserve batching, resource limits, and non-atomic
406/// failure semantics. All helpers compose vectorized Vfsi primitives; native execution belongs in Vfsi.
407/// Single-target helpers use conventional names such as `open`, `metadata`,
408/// and `read_dir`. Prefer vector APIs for independent work on many files or
409/// directories so the backend can batch requests.
410///
411/// # Method groups
412///
413/// - Open and close: [`open`](Self::open), [`create`](Self::create).
414/// - Read files: [`read_files`](Self::read_files), [`read_files_with_options`](Self::read_files_with_options).
415/// - Write files: [`write`](Self::write), [`write_files`](Self::write_files).
416/// - Metadata: [`metadata`](Self::metadata), [`metadata_with_options`](Self::metadata_with_options).
417/// - Collect directories: [`read_dir`](Self::read_dir), [`read_dir_with_options`](Self::read_dir_with_options).
418/// - Visit directories: [`visit_dir`](Self::visit_dir), [`visit_walk`](Self::visit_walk).
419/// - Create, move, copy, and remove: [`create_dir`](Self::create_dir), [`create_dir_all`](Self::create_dir_all).
420///
421/// # Boundary
422///
423/// | Contract | Responsibility | Examples |
424/// | --- | --- | --- |
425/// | [`Vfsi`] | Native vector execution, paging, and policy inspection | `vopen`, `vread`, `vlistdirs`, `limits` |
426/// | `VfsiExt` | Scalar adapters and composed workflows | `open`, `read_files`, `create_dir_all` |
427///
428/// Implement only `Vfsi`; this extension is blanket implemented. A helper belongs
429/// here only when it can compose Vfsi primitives without losing native batching,
430/// bounded paging, or recovery semantics. Singular convenience is not a promise
431/// of one RPC, and vector execution is not a promise of atomicity.
432pub trait VfsiExt: Vfsi {
433 // Open and close
434 /// Single-target convenience. For multiple files, prefer [`Vfsi::vopen`] to expose batching opportunities.
435 ///
436 /// Open read-only. Paths are relative to this client's configured namespace.
437 ///
438 /// Use [`open_with`](VfsiExt::open_with) for write/create flags, or
439 /// [`vopen`](Vfsi::vopen) to batch many opens. The returned handle implements
440 /// `std::io::Read`/`Write`/`Seek`; use native methods to retain structured errors.
441 ///
442 /// ```no_run
443 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, WriteOp};
444 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
445 /// let mut file = fs.open("/config")?;
446 /// let mut header = [0_u8; 128];
447 /// let result = file.read_native(&mut header); // May be short.
448 /// let close = file.close();
449 /// let bytes = result?;
450 /// close?;
451 /// println!("read {bytes} bytes");
452 /// # Ok(())
453 /// # }
454 /// ```
455 fn open(&self, path: impl AsRef<Path>) -> Result<Self::File> {
456 self.open_with(OpenRequest::new(path.as_ref(), crate::api::OpenFlags::READ))
457 }
458
459 /// Single-target convenience. For multiple creations, prefer [`Vfsi::vopen`] with CREATE/TRUNCATE flags.
460 ///
461 /// Create or truncate a file and open for writing; does not create parents.
462 ///
463 /// Existing contents are discarded immediately. For exclusive creation,
464 /// use [`open_with`](VfsiExt::open_with) with `WRITE | CREATE_NEW` instead.
465 ///
466 /// ```no_run
467 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, WriteOp};
468 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
469 /// let file = fs.create("/output")?;
470 /// let result = fs.vwrite(&[WriteOp::at(&file, 0, b"complete contents")], vfsi_core::api::WriteOptions::new().write_all(true));
471 /// let close = file.close();
472 /// result?;
473 /// close?;
474 /// # Ok(())
475 /// # }
476 /// ```
477 fn create(&self, path: impl AsRef<Path>) -> Result<Self::File> {
478 self.open_with(OpenRequest::new(
479 path.as_ref(),
480 crate::api::OpenFlags::WRITE
481 | crate::api::OpenFlags::CREATE
482 | crate::api::OpenFlags::TRUNCATE,
483 ))
484 }
485
486 /// Single-target convenience. For multiple requests, prefer [`Vfsi::vopen`] with per-file flags and modes.
487 ///
488 /// Open with an explicit access/create/truncate request; effects are eager.
489 ///
490 /// Creation/truncation occurs at open, not on the first write. `CREATE_NEW`
491 /// rejects an existing path. This does not create missing parents.
492 /// Delegates to singleton `Vfsi::vopen`. Backends must preserve scalar final-
493 /// symlink resolution and independently opened handles for singleton vectors.
494 ///
495 /// ```no_run
496 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenRequest, WriteOp};
497 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
498 /// let file = fs.open_with(OpenRequest::new(
499 /// "/new-file", OpenFlags::WRITE | OpenFlags::CREATE_NEW,
500 /// ).mode(0o600))?;
501 /// file.close()?;
502 /// # Ok(())
503 /// # }
504 /// ```
505 fn open_with(&self, request: OpenRequest) -> Result<Self::File> {
506 let mut files = self.vopen(&[request])?;
507 if files.len() != 1 {
508 return Err(crate::api::Error::transport(
509 None,
510 "vopen returned an invalid result count",
511 ));
512 }
513 Ok(files.remove(0))
514 }
515
516 /// Consume all handles. Errors cannot return cleanup ownership; Drop is
517 /// best-effort. Prefer `vclose` when close errors require reconciliation.
518 ///
519 /// This releases all local handles even on failure; it does not promise
520 /// every remote CLOSE succeeded. Closing alone is not a durability barrier.
521 ///
522 /// ```no_run
523 /// use vfsi_core::api::{Vfsi, VfsiExt, OpenFlags, OpenRequest, WriteOp};
524 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
525 /// let files = fs.vopen(&[OpenRequest::new("/config", OpenFlags::READ)])?;
526 /// fs.close_files(files)?; // Observe a close error instead of discarding it in Drop.
527 /// # Ok(())
528 /// # }
529 /// ```
530 fn close_files(&self, mut files: Vec<Self::File>) -> Result<()> {
531 self.vclose(&mut files)
532 }
533
534 // Read files
535 /// Read complete files in input order using vectorized whole-file reads.
536 ///
537 /// The aggregate payload is bounded by `limits().max_read_bytes`.
538 /// Use [`Self::read_files_with_options`] to override it, or stream large files.
539 /// A failure returns an error, not a successful partial collection.
540 ///
541 /// ```no_run
542 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
543 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
544 /// let contents = fs.read_files(&["/file-1", "/file-2"])?;
545 /// for bytes in contents { println!("{} bytes", bytes.len()); }
546 /// # Ok(())
547 /// # }
548 /// ```
549 fn read_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<Vec<u8>>> {
550 self.read_files_with_options(paths, crate::api::ReadOptions::default())
551 }
552
553 /// Whole-file convenience reads with an explicit aggregate payload budget.
554 ///
555 /// ```no_run
556 /// use vfsi_core::api::{Vfsi, VfsiExt, ReadOptions, WriteOp};
557 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
558 /// let files = fs.read_files_with_options(&["/config"],
559 /// ReadOptions::new().max_total_bytes(std::num::NonZeroUsize::new(1024 * 1024)))?;
560 /// # let _ = files;
561 /// # Ok(())
562 /// # }
563 /// ```
564 fn read_files_with_options<P: AsRef<Path>>(
565 &self,
566 paths: &[P],
567 options: crate::api::ReadOptions,
568 ) -> Result<Vec<Vec<u8>>> {
569 let results = self.vread(
570 paths
571 .iter()
572 .map(|path| crate::api::ReadOp::whole(path.as_ref())),
573 options,
574 )?;
575 if results.len() != paths.len() {
576 return Err(crate::api::Error::transport(
577 None,
578 "readv returned an invalid result count",
579 ));
580 }
581 results
582 .into_iter()
583 .enumerate()
584 .map(|(index, result)| {
585 let data = result.data.ok_or_else(|| {
586 crate::api::Error::transport(Some(index), "whole-file readv omitted owned data")
587 })?;
588 if data.len() != result.read || result.offset != 0 || !result.eof {
589 return Err(crate::api::Error::transport(
590 Some(index),
591 "whole-file readv returned incomplete or invalid data",
592 ));
593 }
594 Ok(data)
595 })
596 .collect()
597 }
598
599 /// Single-target convenience. For multiple files, prefer [`Vfsi::vread`] with whole-file operations and decode each result as UTF-8.
600 ///
601 /// Read a complete UTF-8 file within this client's aggregate read budget.
602 ///
603 /// Invalid UTF-8 is an invalid-input error; use `read_files` for arbitrary bytes.
604 ///
605 /// ```no_run
606 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
607 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
608 /// let text = fs.read_to_string("/config")?;
609 /// println!("{text}");
610 /// # Ok(())
611 /// # }
612 /// ```
613 fn read_to_string(&self, path: impl AsRef<Path>) -> Result<String> {
614 self.read_to_string_with_options(path, crate::api::ReadOptions::default())
615 }
616
617 /// Single-target convenience. For multiple files, prefer [`Vfsi::vread`] with an aggregate byte budget, then decode UTF-8.
618 ///
619 /// Read a complete UTF-8 file with the same budget policy as `Vfsi::vread`.
620 /// `ReadOptions::default()` inherits the client limit; explicit overrides are nonzero.
621 ///
622 /// ```no_run
623 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
624 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
625 /// let text = fs.read_to_string_with_options("/config", vfsi_core::api::ReadOptions::new()
626 /// .max_total_bytes(std::num::NonZeroUsize::new(4096)))?;
627 /// println!("{text}");
628 /// # Ok(())
629 /// # }
630 /// ```
631 fn read_to_string_with_options(
632 &self,
633 path: impl AsRef<Path>,
634 options: crate::api::ReadOptions,
635 ) -> Result<String> {
636 let path = path.as_ref();
637 let mut files = self.read_files_with_options(&[path], options)?;
638 String::from_utf8(files.remove(0)).map_err(|_| {
639 crate::api::Error::client(0, crate::ERR_INVAL).with_context("read_to_string", path)
640 })
641 }
642
643 /// Single-target convenience. For multiple files, prefer [`Vfsi::vstream`]. Backends may process streams sequentially.
644 ///
645 /// Stream a file using the client's bounded chunk size instead of collecting it.
646 /// Return `Ok(false)` to stop successfully; the callback runs outside the lock.
647 ///
648 /// ```no_run
649 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
650 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
651 /// fs.read_stream("/large", |offset, bytes| {
652 /// println!("{} bytes at {offset}", bytes.len());
653 /// Ok(true)
654 /// })?;
655 /// # Ok(())
656 /// # }
657 /// ```
658 fn read_stream(
659 &self,
660 path: impl AsRef<Path>,
661 callback: impl FnMut(u64, &[u8]) -> Result<bool>,
662 ) -> Result<crate::api::StreamCompletion> {
663 self.read_stream_with_options(
664 path,
665 crate::api::ReadStreamOptions::new().chunk_size(self.limits().stream_chunk_bytes),
666 callback,
667 )
668 }
669
670 /// Single-target convenience. For multiple files, prefer [`Vfsi::vstream`]. Backends may process streams sequentially.
671 ///
672 /// Stream from offset zero outside the backend lock. `false` stops after
673 /// the delivered chunk; completion reports the next offset. Not a snapshot.
674 ///
675 /// The callback borrows each chunk only for its invocation. Consume it
676 /// without collecting chunks to keep memory bounded. `chunk_size` is a
677 /// maximum; negotiated protocol limits and short reads can produce smaller
678 /// chunks. The internally opened file is closed on completion, stopping,
679 /// callback error, or read failure. Stopping successfully does not mean EOF.
680 /// This simple stream does not promise RTT-hiding parallelism; direct NFS
681 /// applications can use `NfsBuilder::connect_read_pool` for pipelined reads.
682 ///
683 /// ```no_run
684 /// use vfsi_core::api::{Vfsi, VfsiExt, ReadStreamOptions, StreamCompletion, WriteOp};
685 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
686 /// let mut processed = 0_u64;
687 /// let completion = fs.read_stream_with_options(
688 /// "/large.bin", ReadStreamOptions::new().chunk_size(1024 * 1024),
689 /// |_offset, chunk| {
690 /// processed += chunk.len() as u64; // Process the borrowed bytes here.
691 /// Ok(processed < 8 * 1024 * 1024) // Stop after a bounded sample.
692 /// },
693 /// )?;
694 /// match completion {
695 /// StreamCompletion::Complete => println!("reached EOF after {processed} bytes"),
696 /// StreamCompletion::Stopped { next_offset } => println!("stopped at {next_offset}"),
697 /// }
698 /// # Ok(())
699 /// # }
700 /// ```
701 fn read_stream_with_options(
702 &self,
703 path: impl AsRef<Path>,
704 options: crate::api::ReadStreamOptions,
705 callback: impl FnMut(u64, &[u8]) -> Result<bool>,
706 ) -> Result<crate::api::StreamCompletion> {
707 let mut callback = callback;
708 single_completion(
709 self.vstream(&[path], options, |_, offset, data| callback(offset, data))?,
710 "read_streams",
711 )
712 }
713
714 // Write files
715 /// Single-target convenience. For multiple complete files, prefer [`VfsiExt::write_files`]; for opened handles, prefer [`Vfsi::vwrite`].
716 ///
717 /// Replace a file completely, creating/truncating eagerly; not atomic replace.
718 ///
719 /// The parent must exist. Success writes all bytes and closes the internal
720 /// handle; failure may leave a created, truncated, or partially written file.
721 /// For exclusive creation or durability control use an explicit open handle.
722 ///
723 /// ```no_run
724 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
725 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
726 /// fs.write("/output", b"replacement contents")?;
727 /// # Ok(())
728 /// # }
729 /// ```
730 fn write(&self, path: impl AsRef<Path>, data: &[u8]) -> Result<()> {
731 // Scalar open preserves backend-specific final-symlink resolution.
732 // Always attempt close, but retain the write error if both fail.
733 let file = self.create(path)?;
734 let result = self.vwrite(
735 &[crate::api::WriteOp::at(&file, 0, data)],
736 crate::api::WriteOptions::new().write_all(true),
737 );
738 let close_result = file.close();
739 result?;
740 close_result
741 }
742
743 /// Replace files in vector phases. Success completes payloads; errors may
744 /// follow create/truncate/write effects and never authorize blind replay.
745 ///
746 /// Parents must exist. Payloads are completed via vector open/write/close
747 /// phases, not a scalar write loop or necessarily a single RPC. No atomic
748 /// replacement, cross-file transaction, or durability guarantee is provided.
749 ///
750 /// ```no_run
751 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
752 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
753 /// fs.write_files(&[
754 /// ("/file-1", b"hello".as_slice()),
755 /// ("/file-2", b"world".as_slice()),
756 /// ])?;
757 /// # Ok(())
758 /// # }
759 /// ```
760 fn write_files<P: AsRef<Path>, B: AsRef<[u8]>>(&self, entries: &[(P, B)]) -> Result<()> {
761 let mut seen = std::collections::HashSet::with_capacity(entries.len());
762 for (index, (path, _)) in entries.iter().enumerate() {
763 if !seen.insert(path.as_ref()) {
764 return Err(crate::api::Error::client(index, crate::ERR_INVAL)
765 .with_context("write_files", path.as_ref()));
766 }
767 }
768 let requests: Vec<_> = entries
769 .iter()
770 .map(|(path, _)| {
771 OpenRequest::new(
772 path.as_ref(),
773 crate::api::OpenFlags::WRITE
774 | crate::api::OpenFlags::CREATE
775 | crate::api::OpenFlags::TRUNCATE,
776 )
777 })
778 .collect();
779 let files = self.vopen(&requests)?;
780 if files.len() != entries.len() {
781 return Err(crate::api::Error::transport(
782 None,
783 "vopen returned an invalid result count",
784 ));
785 }
786 let writes: Vec<_> = files
787 .iter()
788 .zip(entries)
789 .map(|(file, (_, data))| crate::api::WriteOp::at(file, 0, data.as_ref()))
790 .collect();
791 let result = self.vwrite(&writes, crate::api::WriteOptions::new().write_all(true));
792 drop(writes);
793 let close_result = self.close_files(files);
794 result?;
795 close_result
796 }
797
798 // Metadata
799 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`].
800 ///
801 /// Query a path following its final symlink; unavailable fields remain None.
802 ///
803 /// To inspect the symlink itself use [`symlink_metadata`](VfsiExt::symlink_metadata).
804 /// To identify an already opened object after rename, use [`FileHandle::metadata`].
805 ///
806 /// ```no_run
807 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
808 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
809 /// let metadata = fs.metadata("/file-1")?;
810 /// println!("{} bytes; directory={}", metadata.len(), metadata.is_dir());
811 /// # Ok(())
812 /// # }
813 /// ```
814 fn metadata(&self, path: impl AsRef<Path>) -> Result<Metadata> {
815 metadata(self, path, crate::api::MetadataOptions::new())
816 }
817
818 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`] with selected fields and follow_symlinks(false).
819 ///
820 /// Query one path with selected fields and final-symlink behavior; absent fields remain None.
821 ///
822 /// This avoids requesting every optional attribute. Type/size can be
823 /// requested internally even when not selected; missing optional attributes
824 /// must still be handled via their `Option` accessors.
825 ///
826 /// ```no_run
827 /// use vfsi_core::api::{Vfsi, VfsiExt, MetadataFields, WriteOp};
828 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
829 /// let metadata = fs.metadata_with_options(
830 /// "/file-1", vfsi_core::api::MetadataOptions::new()
831 /// .fields(MetadataFields::MODE | MetadataFields::BLOCKS).follow_symlinks(false),
832 /// )?;
833 /// if let Some(blocks) = metadata.blocks() {
834 /// println!("allocated blocks: {blocks}");
835 /// }
836 /// # Ok(())
837 /// # }
838 /// ```
839 fn metadata_with_options(
840 &self,
841 path: impl AsRef<Path>,
842 options: crate::api::MetadataOptions,
843 ) -> Result<Metadata> {
844 metadata(self, path, options)
845 }
846
847 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`] with follow_symlinks(false).
848 ///
849 /// Query the final symlink itself instead of following it.
850 ///
851 /// This does not prevent following symlinks in ancestor components and is
852 /// not a race-free namespace confinement primitive.
853 ///
854 /// ```no_run
855 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
856 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
857 /// let metadata = fs.symlink_metadata("/link-or-file")?;
858 /// println!("symlink={}", metadata.is_symlink());
859 /// # Ok(())
860 /// # }
861 /// ```
862 fn symlink_metadata(&self, path: impl AsRef<Path>) -> Result<Metadata> {
863 self.metadata_with_options(
864 path,
865 crate::api::MetadataOptions::new().follow_symlinks(false),
866 )
867 }
868
869 // Collect directories
870 /// Single-target convenience. For multiple directories, prefer [`VfsiExt::read_dirs_with_options`] or [`VfsiExt::read_dirs`].
871 ///
872 /// Collect one directory using the client's entry and path-byte limits.
873 ///
874 /// ```no_run
875 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
876 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
877 /// for entry in fs.read_dir("/input")? {
878 /// println!("{}", entry.path().display());
879 /// }
880 /// # Ok(())
881 /// # }
882 /// ```
883 fn read_dir(&self, path: impl AsRef<Path>) -> Result<Vec<crate::api::DirEntry>> {
884 self.read_dir_with_options(
885 path,
886 crate::api::VisitOptions::new().fields(crate::api::MetadataFields::stat()),
887 )
888 }
889
890 /// Single-target convenience. For multiple directories, prefer [`VfsiExt::read_dirs_with_options`] with one aggregate budget.
891 ///
892 /// Collect one shallow directory with selected metadata and entry/path-byte limits.
893 /// This helper forces `recursive(false)`; use `walk_with_options` for a tree.
894 ///
895 /// ```no_run
896 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
897 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
898 /// let entries = fs.read_dir_with_options("/input",
899 /// vfsi_core::api::VisitOptions::new().max_entries(100))?;
900 /// println!("{} entries", entries.len());
901 /// # Ok(())
902 /// # }
903 /// ```
904 fn read_dir_with_options(
905 &self,
906 path: impl AsRef<Path>,
907 options: crate::api::VisitOptions,
908 ) -> Result<Vec<crate::api::DirEntry>> {
909 let mut trees = self.read_dirs_with_options(&[path], options.recursive(false))?;
910 let mut listings = single_tree(&mut trees)?;
911 if listings.len() != 1 {
912 return Err(crate::api::Error::transport(
913 None,
914 "read_dirs returned an invalid result count",
915 ));
916 }
917 Ok(listings.remove(0).entries)
918 }
919
920 /// Collect directory listings under one aggregate entry/path-byte policy.
921 ///
922 /// There is one listing per input directory, in input order; entry order is
923 /// backend-defined. Entries include metadata, avoiding a separate scalar
924 /// stat per child. The default policy comes from this client's limits.
925 /// Use [`read_dirs_with_options`](VfsiExt::read_dirs_with_options) to select
926 /// attributes, or a visitor instead of collecting large listings.
927 ///
928 /// ```no_run
929 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
930 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
931 /// for listing in fs.read_dirs(&["/input", "/output"])? {
932 /// for entry in listing.entries {
933 /// println!("{}: {} bytes", entry.path().display(), entry.metadata().len());
934 /// }
935 /// }
936 /// # Ok(())
937 /// # }
938 /// ```
939 fn read_dirs<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<DirectoryListing>> {
940 let trees = self.read_dirs_with_options(
941 paths,
942 crate::api::VisitOptions::new().fields(crate::api::MetadataFields::stat()),
943 )?;
944 if trees.len() != paths.len() || trees.iter().any(|tree| tree.len() != 1) {
945 return Err(crate::api::Error::transport(
946 None,
947 "read_dirs returned an invalid result count",
948 ));
949 }
950 Ok(trees.into_iter().flatten().collect())
951 }
952
953 /// Collect shallow directories or recursive trees, grouped by input root.
954 ///
955 /// `results[i]` contains the listings for `paths[i]`. Shallow mode
956 /// returns exactly one listing per root; recursive mode includes the root
957 /// and descendants. Entries and path-byte budgets are shared across roots;
958 /// recursive collection also charges retained directory paths.
959 /// Collection builds on the batched directory-page visitor.
960 /// Explicit limits override client defaults. Errors discard collected results;
961 /// this is not a snapshot. Symlink entries are not recursively followed.
962 ///
963 /// ```no_run
964 /// use vfsi_core::api::{Vfsi, VfsiExt, MetadataFields, VisitOptions};
965 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
966 /// let trees = fs.read_dirs_with_options(&["/input", "/output"],
967 /// VisitOptions::new().recursive(true).fields(MetadataFields::MODE)
968 /// .max_entries(10_000).max_path_bytes(1024 * 1024))?;
969 /// for tree in trees {
970 /// for listing in tree {
971 /// println!("{}: {} entries", listing.path.display(), listing.entries.len());
972 /// }
973 /// }
974 /// # Ok(())
975 /// # }
976 /// ```
977 fn read_dirs_with_options<P: AsRef<Path>>(
978 &self,
979 paths: &[P],
980 options: crate::api::VisitOptions,
981 ) -> Result<Vec<Vec<DirectoryListing>>> {
982 let mut trees: Vec<Vec<DirectoryListing>> = (0..paths.len()).map(|_| Vec::new()).collect();
983 let mut positions: Vec<std::collections::HashMap<std::path::PathBuf, usize>> = (0..paths
984 .len())
985 .map(|_| std::collections::HashMap::new())
986 .collect();
987 self.vlistdirs(paths, options, |index, page| {
988 let tree = trees
989 .get_mut(index)
990 .ok_or_else(|| crate::api::Error::transport(None, "invalid visitor root index"))?;
991 let positions = &mut positions[index];
992 let slot = *positions.entry(page.path.clone()).or_insert_with(|| {
993 tree.push(DirectoryListing {
994 path: page.path.clone(),
995 entries: Vec::new(),
996 });
997 tree.len() - 1
998 });
999 tree[slot].entries.extend(page.entries);
1000 Ok(std::ops::ControlFlow::Continue(()))
1001 })?;
1002 Ok(trees)
1003 }
1004
1005 /// Single-target convenience. For multiple roots, prefer [`VfsiExt::read_dirs_with_options`].
1006 ///
1007 /// Collect a no-follow tree using the client's entry, byte, and depth limits.
1008 ///
1009 /// ```no_run
1010 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1011 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1012 /// for listing in fs.walk("/project")? {
1013 /// println!("{}", listing.path.display());
1014 /// }
1015 /// # Ok(())
1016 /// # }
1017 /// ```
1018 fn walk(&self, root: impl AsRef<Path>) -> Result<Vec<DirectoryListing>> {
1019 self.walk_with_options(
1020 root,
1021 crate::api::VisitOptions::new().fields(crate::api::MetadataFields::stat()),
1022 )
1023 }
1024
1025 /// Single-target convenience. For multiple roots, prefer [`VfsiExt::read_dirs_with_options`] with one aggregate budget.
1026 ///
1027 /// Collect a bounded tree, without following symlinks; no snapshot promise.
1028 ///
1029 /// Returns directory listings, not one flattened entry vector. Budgets
1030 /// apply across the walk; depth zero is the starting directory. Explicit
1031 /// options override client defaults. This helper forces `recursive(true)`.
1032 /// Use a visitor for incremental delivery
1033 /// or [`walk_events_with_options`](VfsiExt::walk_events_with_options) for pruning.
1034 ///
1035 /// ```no_run
1036 /// use vfsi_core::api::{VisitOptions, Vfsi, VfsiExt, MetadataFields, WalkOptions, WriteOp};
1037 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1038 /// let listings = fs.walk_with_options(
1039 /// "/project", VisitOptions::new().fields(MetadataFields::MODE | MetadataFields::SIZE)
1040 /// .max_entries(10_000).max_path_bytes(1024 * 1024).max_depth(8),
1041 /// )?;
1042 /// println!("{} directory listings", listings.len());
1043 /// # Ok(())
1044 /// # }
1045 /// ```
1046 fn walk_with_options(
1047 &self,
1048 path: impl AsRef<Path>,
1049 options: crate::api::VisitOptions,
1050 ) -> Result<Vec<DirectoryListing>> {
1051 let mut trees = self.read_dirs_with_options(&[path], options.recursive(true))?;
1052 if trees.len() != 1 {
1053 return Err(crate::api::Error::transport(
1054 None,
1055 "walks returned an invalid result count",
1056 ));
1057 }
1058 Ok(trees.remove(0))
1059 }
1060
1061 // Visit directories
1062 /// Single-target convenience. For multiple directories, prefer [`Vfsi::vlistdirs`].
1063 ///
1064 /// Visit a directory incrementally using the client's allocation limits.
1065 /// The callback runs outside the backend lock.
1066 ///
1067 /// ```no_run
1068 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1069 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1070 /// fs.visit_dir("/input", |entry| {
1071 /// println!("{}", entry.path().display());
1072 /// Ok(std::ops::ControlFlow::Continue(()))
1073 /// })?;
1074 /// # Ok(())
1075 /// # }
1076 /// ```
1077 fn visit_dir(
1078 &self,
1079 path: impl AsRef<Path>,
1080 callback: impl FnMut(&crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
1081 ) -> Result<crate::api::TraversalCompletion> {
1082 self.visit_dir_with_options(path, crate::api::VisitOptions::new(), callback)
1083 }
1084
1085 /// Single-target convenience. For multiple roots, prefer [`Vfsi::vlistdirs`].
1086 ///
1087 /// Visit a no-follow tree incrementally using the client's traversal limits.
1088 /// The callback runs outside the backend lock.
1089 ///
1090 /// ```no_run
1091 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1092 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1093 /// fs.visit_walk("/project", |entry| {
1094 /// println!("{}", entry.path().display());
1095 /// Ok(std::ops::ControlFlow::Continue(()))
1096 /// })?;
1097 /// # Ok(())
1098 /// # }
1099 /// ```
1100 fn visit_walk(
1101 &self,
1102 root: impl AsRef<Path>,
1103 callback: impl FnMut(&crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
1104 ) -> Result<crate::api::TraversalCompletion> {
1105 self.visit_dir_with_options(
1106 root,
1107 crate::api::VisitOptions::new().recursive(true),
1108 callback,
1109 )
1110 }
1111
1112 /// Single-target convenience. For multiple directories, prefer [`Vfsi::vlistdirs`].
1113 ///
1114 /// Visit one directory; `Break(())` returns Stopped, exhaustion returns
1115 /// Complete, and callback errors propagate. The callback may reenter.
1116 ///
1117 /// Defaults to immediate children. Set `recursive(true)` to visit descendants
1118 /// without following entry symlinks. Options are forwarded unchanged, including
1119 /// entry, path-byte, and depth limits. Paging bounds incremental delivery;
1120 /// backends without paging may buffer one bounded listing. Entry order is
1121 /// backend-defined. `Break(())` stops the entire traversal, not one subtree.
1122 /// To prune a subtree, use [`walk_events_with_options`](Self::walk_events_with_options).
1123 ///
1124 /// ```no_run
1125 /// use vfsi_core::api::{VisitOptions, Vfsi, VfsiExt, ControlFlow, WriteOp};
1126 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1127 /// let completion = fs.visit_dir_with_options(
1128 /// "/input", VisitOptions::new().max_entries(10_000), |entry| {
1129 /// println!("{}", entry.path().display());
1130 /// Ok(ControlFlow::Continue(()))
1131 /// },
1132 /// )?;
1133 /// # let _ = completion;
1134 /// # Ok(())
1135 /// # }
1136 /// ```
1137 ///
1138 /// Recursive traversal uses the same entry point:
1139 ///
1140 /// ```no_run
1141 /// use vfsi_core::api::{ControlFlow, VfsiExt, VisitOptions};
1142 /// # fn example(fs: &impl vfsi_core::Vfsi) -> vfsi_core::api::Result<()> {
1143 /// fs.visit_dir_with_options("/project", VisitOptions::new().recursive(true), |entry| {
1144 /// println!("{}", entry.path().display());
1145 /// Ok(ControlFlow::Continue(()))
1146 /// })?;
1147 /// # Ok(())
1148 /// # }
1149 /// ```
1150 fn visit_dir_with_options(
1151 &self,
1152 path: impl AsRef<Path>,
1153 options: crate::api::VisitOptions,
1154 callback: impl FnMut(&crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
1155 ) -> Result<crate::api::TraversalCompletion> {
1156 let mut callback = callback;
1157 single_completion(
1158 self.visit_entries_with_options(&[path], options, |_, entry| callback(entry))?,
1159 "visit_dirs",
1160 )
1161 }
1162
1163 /// Visit individual entries using the directory-page primitive.
1164 /// Empty directories produce pages but no entry callbacks.
1165 fn visit_entries_with_options<P: AsRef<Path>>(
1166 &self,
1167 paths: &[P],
1168 options: crate::api::VisitOptions,
1169 mut callback: impl FnMut(usize, &crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
1170 ) -> Result<Vec<crate::api::TraversalCompletion>> {
1171 self.vlistdirs(paths, options, |index, page| {
1172 for entry in &page.entries {
1173 if callback(index, entry)?.is_break() {
1174 return Ok(std::ops::ControlFlow::Break(()));
1175 }
1176 }
1177 Ok(std::ops::ControlFlow::Continue(()))
1178 })
1179 }
1180
1181 /// Single-target convenience. For multiple roots without enter/leave events or subtree pruning, prefer [`Vfsi::vlistdirs`]. Keep this helper when those event semantics are required.
1182 ///
1183 /// Incremental no-follow traversal with enter/leave events and pruning.
1184 /// Listings are bounded by the remaining aggregate budget; the callback
1185 /// runs before entering each directory, so pruning avoids its listing.
1186 ///
1187 /// `Enter`/`Leave` surround directories (including pruned ones); symlinks
1188 /// are `Entry` events, never followed. `SkipSubtree` is meaningful on `Enter`;
1189 /// `Stop` stops the entire walk. `sort_by_name` sorts siblings, not the whole
1190 /// tree. Limits charge fetched entries, including the root, even if pruned.
1191 /// Unlike a flat visit, this retains a bounded active frontier/listing.
1192 ///
1193 /// # Example: skip `.git` before listing it
1194 ///
1195 /// ```no_run
1196 /// use vfsi_core::api::{VisitOptions, Vfsi, VfsiExt, MetadataFields, WalkControl, WalkEventKind, WalkOptions, WriteOp};
1197 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1198 /// let completion = fs.walk_events_with_options(
1199 /// "/project", MetadataFields::MODE, WalkOptions::new(), true,
1200 /// |event| {
1201 /// if event.kind == WalkEventKind::Enter
1202 /// && event.entry.file_name() == Some(std::ffi::OsStr::new(".git")) {
1203 /// return Ok(WalkControl::SkipSubtree);
1204 /// }
1205 /// println!("{:?}: {}", event.kind, event.entry.path().display());
1206 /// Ok(WalkControl::Continue)
1207 /// },
1208 /// )?;
1209 /// # let _ = completion;
1210 /// # Ok(())
1211 /// # }
1212 /// ```
1213 fn walk_events_with_options(
1214 &self,
1215 root: impl AsRef<Path>,
1216 fields: crate::api::MetadataFields,
1217 options: crate::api::WalkOptions,
1218 sort_by_name: bool,
1219 callback: impl FnMut(&crate::api::WalkEvent) -> Result<crate::api::WalkControl>,
1220 ) -> Result<crate::api::TraversalCompletion> {
1221 let root = root.as_ref();
1222 let fields = fields | crate::api::MetadataFields::MODE;
1223 let metadata = self.metadata_with_options(
1224 root,
1225 crate::api::MetadataOptions::new()
1226 .fields(fields)
1227 .follow_symlinks(false),
1228 )?;
1229 crate::api::walk_events(
1230 crate::api::DirEntry::new(root.to_path_buf(), metadata),
1231 options,
1232 sort_by_name,
1233 |path, limits| {
1234 let mut trees = self.read_dirs_with_options(
1235 &[path],
1236 crate::api::VisitOptions::from(limits).fields(fields),
1237 )?;
1238 let mut listings = single_tree(&mut trees)?;
1239 if listings.len() != 1 {
1240 return Err(crate::api::Error::transport(
1241 None,
1242 "invalid directory result count",
1243 ));
1244 }
1245 Ok(listings.remove(0).entries)
1246 },
1247 callback,
1248 )
1249 }
1250
1251 // Create, move, copy, and remove
1252 /// Single-target convenience. For multiple independent directories, prefer [`Vfsi::vmkdir`].
1253 ///
1254 /// Create one directory; its parent must exist.
1255 ///
1256 /// An existing entry is an error, even if already a directory. Use
1257 /// [`create_dir_all`](VfsiExt::create_dir_all) for missing parents/idempotent
1258 /// directory setup, or [`vmkdir`](Vfsi::vmkdir) for independent siblings.
1259 ///
1260 /// ```no_run
1261 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1262 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1263 /// fs.create_dir("/fresh-directory")?;
1264 /// # Ok(())
1265 /// # }
1266 /// ```
1267 fn create_dir(&self, path: impl AsRef<Path>) -> Result<()> {
1268 self.vmkdir(&[path])
1269 }
1270
1271 /// Single-target convenience. For multiple directory creations, prefer [`Vfsi::vmkdir`]; plan missing parents before their children.
1272 ///
1273 /// Create missing parents; an error can leave some directories created.
1274 ///
1275 /// Existing directories are accepted; an existing non-directory component
1276 /// is an error. This is not atomic and is not protected against concurrent
1277 /// changes to ancestor paths.
1278 ///
1279 /// ```no_run
1280 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1281 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1282 /// fs.create_dir_all("/workspace/results/2026")?;
1283 /// # Ok(())
1284 /// # }
1285 /// ```
1286 fn create_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
1287 let mut current = std::path::PathBuf::new();
1288 for component in path.as_ref().components() {
1289 match component {
1290 std::path::Component::RootDir => {
1291 current.push(Path::new("/"));
1292 continue;
1293 }
1294 std::path::Component::Normal(part) => current.push(part),
1295 std::path::Component::CurDir => continue,
1296 std::path::Component::ParentDir => {
1297 current.push("..");
1298 continue;
1299 }
1300 std::path::Component::Prefix(_) => {
1301 return Err(crate::api::Error::client(0, crate::ERR_INVAL));
1302 }
1303 }
1304 match self.vmkdir(&[¤t]) {
1305 Ok(()) => {}
1306 Err(error) if error.err_no() == crate::ERR_EXIST => {
1307 if !self.metadata(¤t)?.is_dir() {
1308 return Err(crate::api::Error::client(0, crate::ERR_NOTDIR)
1309 .with_context("create_dir_all", ¤t));
1310 }
1311 }
1312 Err(error) => return Err(error),
1313 }
1314 }
1315 Ok(())
1316 }
1317
1318 /// Single-target convenience. For multiple source/destination pairs, prefer [`Vfsi::vrename`].
1319 ///
1320 /// Rename within supported namespaces; cross-filesystem moves can fail.
1321 ///
1322 /// Both paths use this client's namespace. Replacement semantics follow
1323 /// the backend/filesystem; this is not a cross-file vector transaction or
1324 /// a guarantee of durable directory updates. No copy fallback is implied.
1325 ///
1326 /// ```no_run
1327 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1328 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1329 /// fs.rename("/old-name", "/new-name")?;
1330 /// # Ok(())
1331 /// # }
1332 /// ```
1333 fn rename(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
1334 self.vrename(&[(source, destination)])
1335 }
1336
1337 /// Single-target convenience. For multiple file pairs, prefer [`Vfsi::vcopy`].
1338 ///
1339 /// Copy a file's contents; this is not recursive tree copying.
1340 ///
1341 /// Destination creation/replacement and failures follow backend semantics.
1342 /// A failure can leave a partial destination. Do not assume metadata,
1343 /// sparse layout, or durability is preserved. Server-side copy is used
1344 /// only where supported; callers need not build protocol COPY requests.
1345 ///
1346 /// ```no_run
1347 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1348 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1349 /// fs.copy("/source", "/destination")?;
1350 /// # Ok(())
1351 /// # }
1352 /// ```
1353 fn copy(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
1354 self.vcopy(&[(source, destination)])
1355 }
1356
1357 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API accepts both files and directories.
1358 ///
1359 /// Remove one file or symlink, not the symlink target.
1360 ///
1361 /// Missing paths are errors. To remove a directory use
1362 /// [`remove_dir`](VfsiExt::remove_dir) or [`remove_dir_all`](VfsiExt::remove_dir_all).
1363 ///
1364 /// ```no_run
1365 /// use vfsi_core::api::{Vfsi, VfsiExt, ErrorKind, WriteOp};
1366 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1367 /// match fs.remove_file("/temporary-file") {
1368 /// Ok(()) => (),
1369 /// Err(error) if error.kind() == ErrorKind::NotFound => (),
1370 /// Err(error) => return Err(error),
1371 /// }
1372 /// # Ok(())
1373 /// # }
1374 /// ```
1375 fn remove_file(&self, path: impl AsRef<Path>) -> Result<()> {
1376 let path = path.as_ref();
1377 if self.symlink_metadata(path)?.is_dir() {
1378 return Err(
1379 crate::api::Error::client(0, crate::ERR_ISDIR).with_context("remove_file", path)
1380 );
1381 }
1382 self.vremove(
1383 &[path],
1384 crate::api::RemoveMode::Entry,
1385 crate::api::RemoveOptions::new(),
1386 )
1387 }
1388
1389 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API does not enforce directory-only inputs.
1390 ///
1391 /// Remove one empty directory; not a recursive operation.
1392 ///
1393 /// Nonempty directories fail. Use [`remove_dir_contents`](VfsiExt::remove_dir_contents)
1394 /// to empty a directory while retaining it, or [`remove_dir_all`](VfsiExt::remove_dir_all)
1395 /// to remove its tree. Only operate on paths whose removal you intend.
1396 ///
1397 /// ```no_run
1398 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1399 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1400 /// fs.remove_dir("/empty-temporary-directory")?;
1401 /// # Ok(())
1402 /// # }
1403 /// ```
1404 fn remove_dir(&self, path: impl AsRef<Path>) -> Result<()> {
1405 let path = path.as_ref();
1406 require_directory(self, path, "remove_dir")?;
1407 self.vremove(
1408 &[path],
1409 crate::api::RemoveMode::Entry,
1410 crate::api::RemoveOptions::new(),
1411 )
1412 }
1413
1414 /// Single-target convenience. For multiple trees, prefer [`Vfsi::vremove`] with `RemoveMode::Tree`.
1415 ///
1416 /// Recursively remove a tree without following directory symlinks.
1417 /// Path-based removal is not a security sandbox or an atomic transaction.
1418 ///
1419 /// A failure can leave a partly removed tree. Use trusted ancestor paths;
1420 /// concurrent namespace substitutions are not a confinement guarantee.
1421 /// This is destructive: do not use it to prepare an arbitrary existing
1422 /// directory for an example or benchmark.
1423 ///
1424 /// ```no_run
1425 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1426 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1427 /// // Remove only the tree that this application intentionally owns.
1428 /// fs.remove_dir_all("/owned-temporary-tree")?;
1429 /// # Ok(())
1430 /// # }
1431 /// ```
1432 fn remove_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
1433 let path = path.as_ref();
1434 require_directory(self, path, "remove_dir_all")?;
1435 self.vremove(
1436 &[path],
1437 crate::api::RemoveMode::Tree,
1438 crate::api::RemoveOptions::new(),
1439 )
1440 }
1441
1442 /// Single-target convenience. For multiple directory roots, prefer [`Vfsi::vremove`].
1443 ///
1444 /// Recursively empty a directory while keeping its root.
1445 ///
1446 /// The root must be a directory, not a symlink. Child symlinks are removed
1447 /// without following their targets. Errors can leave partially removed
1448 /// contents; retaining the root does not make the operation transactional.
1449 ///
1450 /// ```no_run
1451 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1452 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1453 /// fs.remove_dir_contents("/owned-scratch-directory")?;
1454 /// // The scratch directory remains available for subsequent work.
1455 /// # Ok(())
1456 /// # }
1457 /// ```
1458 fn remove_dir_contents(&self, path: impl AsRef<Path>) -> Result<()> {
1459 self.vremove(
1460 &[path],
1461 crate::api::RemoveMode::Contents,
1462 crate::api::RemoveOptions::new(),
1463 )
1464 }
1465}
1466
1467impl<C: Vfsi + ?Sized> VfsiExt for C {}
1468
1469fn single_completion<T>(mut values: Vec<T>, operation: &'static str) -> Result<T> {
1470 if values.len() != 1 {
1471 return Err(crate::api::Error::transport(
1472 None,
1473 format!("{operation} returned an invalid result count"),
1474 ));
1475 }
1476 Ok(values.remove(0))
1477}
1478fn require_directory<C: Vfsi + ?Sized>(fs: &C, path: &Path, operation: &'static str) -> Result<()> {
1479 if !fs.symlink_metadata(path)?.is_dir() {
1480 return Err(crate::api::Error::client(0, crate::ERR_NOTDIR).with_context(operation, path));
1481 }
1482 Ok(())
1483}
1484fn metadata<C: Vfsi + ?Sized>(
1485 client: &C,
1486 path: impl AsRef<Path>,
1487 options: crate::api::MetadataOptions,
1488) -> Result<Metadata> {
1489 let mut results = client.vgetattrs(&[path], options)?;
1490 if results.len() != 1 {
1491 return Err(crate::api::Error::transport(
1492 None,
1493 "vgetattrs returned an invalid result count",
1494 ));
1495 }
1496 Ok(results.remove(0))
1497}
1498
1499fn single_tree(trees: &mut Vec<Vec<DirectoryListing>>) -> Result<Vec<DirectoryListing>> {
1500 if trees.len() != 1 {
1501 return Err(crate::api::Error::transport(
1502 None,
1503 "read_dirs returned an invalid root count",
1504 ));
1505 }
1506 Ok(trees.remove(0))
1507}