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