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