vfsi_core/api/extensions.rs
1use crate::api::{Attrs, CopyOptions, DirectoryListing, MkDirOp, OpenOp, Result, SetAttrsOp};
2use std::path::Path;
3
4use super::{FileHandle, Vfsi};
5
6/// Scalar operations and convenience workflows alongside the [`Vfsi`] vectors.
7///
8/// Blanket-implemented for every filesystem implementing Vfsi. Import `VfsiExt` (or
9/// [`crate::api::prelude`]) to use these helpers; generic code needs only an
10/// `Vfsi` bound. Helpers preserve batching, resource limits, and non-atomic
11/// failure semantics. All helpers compose vectorized Vfsi primitives; native execution belongs in Vfsi.
12/// Single-target helpers use conventional names such as `open`, `attrs`,
13/// and `read_dir`. Prefer vector APIs for independent work on many files or
14/// directories so the backend can batch requests.
15///
16/// # Method groups
17///
18/// - Open and close: [`open`](Self::open), [`create`](Self::create).
19/// - Read files: [`read_files`](Self::read_files), [`read_files_with_options`](Self::read_files_with_options).
20/// - Write files: [`write`](Self::write), [`write_files`](Self::write_files).
21/// - Attrs: [`attrs`](Self::attrs), [`attrs_with_options`](Self::attrs_with_options).
22/// - Collect directories: [`read_dir`](Self::read_dir), [`read_dir_with_options`](Self::read_dir_with_options).
23/// - Visit directories: [`listdir`](Self::listdir), [`visit_dirs_ordered`](Self::visit_dirs_ordered).
24/// - Create, move, copy, and remove: [`create_dir`](Self::create_dir), [`create_dir_all`](Self::create_dir_all).
25///
26/// # Boundary
27///
28/// | Contract | Responsibility | Examples |
29/// | --- | --- | --- |
30/// | [`Vfsi`] | Native vector execution, paging, and policy inspection | `vopen`, `vread`, `vlistdirs`, `limits` |
31/// | `VfsiExt` | Scalar adapters and composed workflows | `open`, `read_files`, `create_dir_all` |
32///
33/// Implement only `Vfsi`; this extension is blanket implemented. A helper belongs
34/// here only when it can compose Vfsi primitives without losing native batching,
35/// bounded paging, or recovery semantics. Singular convenience is not a promise
36/// of one RPC, and vector execution is not a promise of atomicity.
37pub trait VfsiExt: Vfsi {
38 /// Build reusable open options for this client.
39 fn open_options(&self) -> super::OpenOptions<'_, Self> {
40 super::OpenOptions::new(self)
41 }
42
43 /// Query one target through the vector filesystem-statistics engine.
44 fn statfs<P: crate::AsTarget<Self::File>>(&self, target: P) -> Result<crate::FilesystemStats> {
45 single_completion(self.vstatfs(&[target])?, "statfs backend")
46 }
47
48 // Open and close
49 /// Single-target convenience. For multiple files, prefer [`Vfsi::vopen`] to expose batching opportunities.
50 ///
51 /// Open read-only. Paths are relative to this client's configured namespace.
52 ///
53 /// Use [`open_with`](VfsiExt::open_with) for write/create flags, or
54 /// [`vopen`](Vfsi::vopen) to batch many opens. The returned handle owns
55 /// lifecycle only; use vectors or [`std_io`](VfsiExt::std_io) for I/O.
56 ///
57 /// ```no_run
58 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, WriteOp};
59 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
60 /// let file = fs.open("/config")?;
61 /// let mut header = [0_u8; 128];
62 /// let result = fs.vread([vfsi_core::api::ReadOp::into(&file, 0, &mut header)], Default::default());
63 /// let close = file.close();
64 /// let bytes = result?[0].bytes_read();
65 /// close?;
66 /// println!("read {bytes} bytes");
67 /// # Ok(())
68 /// # }
69 /// ```
70 fn open(&self, path: impl AsRef<Path>) -> Result<Self::File> {
71 self.open_with(OpenOp::new(path.as_ref(), crate::api::OpenFlags::READ))
72 }
73
74 /// Single-target convenience. For multiple creations, prefer [`Vfsi::vopen`] with CREATE/TRUNCATE flags.
75 ///
76 /// Create or truncate a file and open for writing; does not create parents.
77 ///
78 /// Existing contents are discarded immediately. For exclusive creation,
79 /// use [`open_with`](VfsiExt::open_with) with `WRITE | CREATE_NEW` instead.
80 ///
81 /// ```no_run
82 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, WriteOp};
83 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
84 /// let file = fs.create("/output")?;
85 /// let result = fs.vwrite(&[WriteOp::at(&file, 0, b"complete contents")], vfsi_core::api::WriteOptions::new().with_write_all(true));
86 /// let close = file.close();
87 /// result?;
88 /// close?;
89 /// # Ok(())
90 /// # }
91 /// ```
92 fn create(&self, path: impl AsRef<Path>) -> Result<Self::File> {
93 self.open_with(OpenOp::new(
94 path.as_ref(),
95 crate::api::OpenFlags::WRITE
96 | crate::api::OpenFlags::CREATE
97 | crate::api::OpenFlags::TRUNCATE,
98 ))
99 }
100
101 /// Single-target convenience. For multiple requests, prefer [`Vfsi::vopen`] with per-file flags and modes.
102 ///
103 /// Open with an explicit access/create/truncate request; effects are eager.
104 ///
105 /// Creation/truncation occurs at open, not on the first write. `CREATE_NEW`
106 /// rejects an existing path. This does not create missing parents.
107 /// Delegates to singleton `Vfsi::vopen`. Backends must preserve scalar final-
108 /// symlink resolution and independently opened handles for singleton vectors.
109 ///
110 /// ```no_run
111 /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenOp, WriteOp};
112 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
113 /// let file = fs.open_with(OpenOp::new(
114 /// "/new-file", OpenFlags::WRITE | OpenFlags::CREATE_NEW,
115 /// ).mode(0o600))?;
116 /// file.close()?;
117 /// # Ok(())
118 /// # }
119 /// ```
120 fn open_with(&self, request: OpenOp) -> Result<Self::File> {
121 single_completion(self.vopen(&[request])?, "vopen")
122 }
123
124 /// Consume all handles. Errors cannot return cleanup ownership; Drop is
125 /// best-effort. Prefer `vclose` when close errors require reconciliation.
126 ///
127 /// This releases all local handles even on failure; it does not promise
128 /// every remote CLOSE succeeded. Closing alone is not a durability barrier.
129 ///
130 /// ```no_run
131 /// use vfsi_core::api::{Vfsi, VfsiExt, OpenFlags, OpenOp, WriteOp};
132 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
133 /// let files = fs.vopen(&[OpenOp::new("/config", OpenFlags::READ)])?;
134 /// fs.close_files(files)?; // Observe a close error instead of discarding it in Drop.
135 /// # Ok(())
136 /// # }
137 /// ```
138 fn close_files(&self, mut files: Vec<Self::File>) -> Result<()> {
139 self.vclose(&mut files)
140 }
141
142 // Read files
143 /// Read one complete file within the client's aggregate read budget.
144 ///
145 /// For multiple files, prefer [`Self::read_files`]. This is a convenience
146 /// over the vector read operation and retains its allocation limits.
147 fn read(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
148 self.read_with_options(path, crate::api::ReadOptions::default())
149 }
150
151 /// Read one complete file with an explicit read policy.
152 fn read_with_options(
153 &self,
154 path: impl AsRef<Path>,
155 options: crate::api::ReadOptions,
156 ) -> Result<Vec<u8>> {
157 let path = path.as_ref();
158 let files = self
159 .read_files_with_options(&[path], options)
160 .map_err(|error| error.with_context("read", path))?;
161 single_completion(files, "read")
162 }
163 /// Read complete files in input order using vectorized whole-file reads.
164 ///
165 /// The aggregate payload is bounded by `limits().max_read_bytes()`.
166 /// Use [`Self::read_files_with_options`] to override it, or stream large files.
167 /// A failure returns an error, not a successful partial collection.
168 ///
169 /// ```no_run
170 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
171 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
172 /// let contents = fs.read_files(&["/file-1", "/file-2"])?;
173 /// for bytes in contents { println!("{} bytes", bytes.len()); }
174 /// # Ok(())
175 /// # }
176 /// ```
177 fn read_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<Vec<u8>>> {
178 self.read_files_with_options(paths, crate::api::ReadOptions::default())
179 }
180
181 /// Whole-file convenience reads with an explicit aggregate payload budget.
182 ///
183 /// ```no_run
184 /// use vfsi_core::api::{Vfsi, VfsiExt, ReadOptions, WriteOp};
185 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
186 /// let files = fs.read_files_with_options(&["/config"],
187 /// ReadOptions::new().with_max_total_bytes(std::num::NonZeroUsize::new(1024 * 1024).unwrap()))?;
188 /// # let _ = files;
189 /// # Ok(())
190 /// # }
191 /// ```
192 fn read_files_with_options<P: AsRef<Path>>(
193 &self,
194 paths: &[P],
195 options: crate::api::ReadOptions,
196 ) -> Result<Vec<Vec<u8>>> {
197 let results = self.vread(
198 paths
199 .iter()
200 .map(|path| crate::api::ReadOp::whole(path.as_ref())),
201 options,
202 )?;
203 if results.len() != paths.len() {
204 return Err(crate::api::Error::transport(
205 None,
206 "vread_native returned an invalid result count",
207 ));
208 }
209 results
210 .into_iter()
211 .enumerate()
212 .map(|(index, result)| {
213 let complete = result.offset() == 0 && result.eof();
214 let data = result.into_data().ok_or_else(|| {
215 crate::api::Error::transport(
216 Some(index),
217 "whole-file vread_native omitted owned data",
218 )
219 })?;
220 if !complete {
221 return Err(crate::api::Error::transport(
222 Some(index),
223 "whole-file vread_native returned incomplete or invalid data",
224 ));
225 }
226 Ok(data)
227 })
228 .collect()
229 }
230
231 /// Single-target convenience. For multiple files, prefer [`Vfsi::vread`] with whole-file operations and decode each result as UTF-8.
232 ///
233 /// Read a complete UTF-8 file within this client's aggregate read budget.
234 ///
235 /// Invalid UTF-8 is an invalid-input error; use `read_files` for arbitrary bytes.
236 ///
237 /// ```no_run
238 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
239 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
240 /// let text = fs.read_to_string("/config")?;
241 /// println!("{text}");
242 /// # Ok(())
243 /// # }
244 /// ```
245 fn read_to_string(&self, path: impl AsRef<Path>) -> Result<String> {
246 self.read_to_string_with_options(path, crate::api::ReadOptions::default())
247 }
248
249 /// Single-target convenience. For multiple files, prefer [`Vfsi::vread`] with an aggregate byte budget, then decode UTF-8.
250 ///
251 /// Read a complete UTF-8 file with the same budget policy as `Vfsi::vread`.
252 /// `ReadOptions::default()` inherits the client limit; explicit overrides are nonzero.
253 ///
254 /// ```no_run
255 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
256 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
257 /// let text = fs.read_to_string_with_options("/config", vfsi_core::api::ReadOptions::new()
258 /// .with_max_total_bytes(std::num::NonZeroUsize::new(4096).unwrap()))?;
259 /// println!("{text}");
260 /// # Ok(())
261 /// # }
262 /// ```
263 fn read_to_string_with_options(
264 &self,
265 path: impl AsRef<Path>,
266 options: crate::api::ReadOptions,
267 ) -> Result<String> {
268 let path = path.as_ref();
269 let mut files = self.read_files_with_options(&[path], options)?;
270 String::from_utf8(files.remove(0)).map_err(|_| {
271 crate::api::Error::client(0, crate::ERR_INVAL).with_context("read_to_string", path)
272 })
273 }
274
275 /// Single-target convenience. For multiple files, prefer [`Vfsi::vstream`]. Backends may process streams sequentially.
276 ///
277 /// Stream a file using the client's bounded chunk size instead of collecting it.
278 /// Return `Ok(ControlFlow::Break(()))` to stop successfully; the callback runs outside the lock.
279 ///
280 /// ```no_run
281 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
282 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
283 /// fs.read_stream("/large", |offset, bytes| {
284 /// println!("{} bytes at {offset}", bytes.len());
285 /// Ok(std::ops::ControlFlow::Continue(()))
286 /// })?;
287 /// # Ok(())
288 /// # }
289 /// ```
290 fn read_stream(
291 &self,
292 path: impl AsRef<Path>,
293 callback: impl FnMut(u64, &[u8]) -> Result<std::ops::ControlFlow<()>>,
294 ) -> Result<crate::api::StreamCompletion> {
295 self.read_stream_with_options(
296 path,
297 crate::api::StreamOptions::new().with_chunk_size(
298 std::num::NonZeroUsize::new(self.limits().stream_chunk_size()).unwrap(),
299 ),
300 callback,
301 )
302 }
303
304 /// Single-target convenience. For multiple files, prefer [`Vfsi::vstream`]. Backends may process streams sequentially.
305 ///
306 /// Stream from offset zero outside the backend lock. `ControlFlow::Break(())` stops after
307 /// the delivered chunk; completion reports the next offset. Not a snapshot.
308 ///
309 /// The callback borrows each chunk only for its invocation. Consume it
310 /// without collecting chunks to keep memory bounded. `chunk_size` is a
311 /// maximum; negotiated protocol limits and short reads can produce smaller
312 /// chunks. The internally opened file is closed on completion, stopping,
313 /// callback error, or read failure. Stopping successfully does not mean EOF.
314 /// This simple stream does not promise RTT-hiding parallelism; direct NFS
315 /// applications can use `NfsBuilder::connect_read_pool` for pipelined reads.
316 ///
317 /// ```no_run
318 /// use vfsi_core::api::{Vfsi, VfsiExt, StreamOptions, StreamCompletion, WriteOp};
319 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
320 /// let mut processed = 0_u64;
321 /// let completion = fs.read_stream_with_options(
322 /// "/large.bin", StreamOptions::new().with_chunk_size(std::num::NonZeroUsize::new(1024 * 1024).unwrap()),
323 /// |_offset, chunk| {
324 /// processed += chunk.len() as u64; // Process the borrowed bytes here.
325 /// Ok(if processed < 8 * 1024 * 1024 {
326 /// std::ops::ControlFlow::Continue(())
327 /// } else {
328 /// std::ops::ControlFlow::Break(())
329 /// }) // Stop after a bounded sample.
330 /// },
331 /// )?;
332 /// match completion {
333 /// StreamCompletion::Complete => println!("reached EOF after {processed} bytes"),
334 /// StreamCompletion::Stopped { next_offset } => println!("stopped at {next_offset}"),
335 /// }
336 /// # Ok(())
337 /// # }
338 /// ```
339 fn read_stream_with_options(
340 &self,
341 path: impl AsRef<Path>,
342 options: crate::api::StreamOptions,
343 callback: impl FnMut(u64, &[u8]) -> Result<std::ops::ControlFlow<()>>,
344 ) -> Result<crate::api::StreamCompletion> {
345 let mut callback = callback;
346 single_completion(
347 self.vstream(&[path], options, |_, offset, data| callback(offset, data))?,
348 "read_streams",
349 )
350 }
351
352 // Write files
353 /// Single-target convenience. For multiple complete files, prefer [`VfsiExt::write_files`]; for opened handles, prefer [`Vfsi::vwrite`].
354 ///
355 /// Replace a file completely, creating/truncating eagerly; not atomic replace.
356 ///
357 /// The parent must exist. Success writes all bytes and closes the internal
358 /// handle; failure may leave a created, truncated, or partially written file.
359 /// For exclusive creation or durability control use an explicit open handle.
360 ///
361 /// ```no_run
362 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
363 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
364 /// fs.write("/output", b"replacement contents")?;
365 /// # Ok(())
366 /// # }
367 /// ```
368 fn write(&self, path: impl AsRef<Path>, data: &[u8]) -> Result<()> {
369 // Scalar open preserves backend-specific final-symlink resolution.
370 // Always attempt close, but retain the write error if both fail.
371 let file = self.create(path)?;
372 let result = self.vwrite(
373 &[crate::api::WriteOp::at(&file, 0, data)],
374 crate::api::WriteOptions::new().with_write_all(true),
375 );
376 let close_result = file.close();
377 result?;
378 close_result
379 }
380
381 /// Replace files in vector phases. Success completes payloads; errors may
382 /// follow create/truncate/write effects and never authorize blind replay.
383 ///
384 /// Parents must exist. Payloads are completed via vector open/write/close
385 /// phases, not a scalar write loop or necessarily a single RPC. No atomic
386 /// replacement, cross-file transaction, or durability guarantee is provided.
387 ///
388 /// ```no_run
389 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
390 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
391 /// fs.write_files(&[
392 /// ("/file-1", b"hello".as_slice()),
393 /// ("/file-2", b"world".as_slice()),
394 /// ])?;
395 /// # Ok(())
396 /// # }
397 /// ```
398 fn write_files<P: AsRef<Path>, B: AsRef<[u8]>>(&self, entries: &[(P, B)]) -> Result<()> {
399 let mut seen = std::collections::HashSet::with_capacity(entries.len());
400 for (index, (path, _)) in entries.iter().enumerate() {
401 if !seen.insert(path.as_ref()) {
402 return Err(crate::api::Error::client(index, crate::ERR_INVAL)
403 .with_context("write_files", path.as_ref()));
404 }
405 }
406 let requests: Vec<_> = entries
407 .iter()
408 .map(|(path, _)| {
409 OpenOp::new(
410 path.as_ref(),
411 crate::api::OpenFlags::WRITE
412 | crate::api::OpenFlags::CREATE
413 | crate::api::OpenFlags::TRUNCATE,
414 )
415 })
416 .collect();
417 let files = self.vopen(&requests)?;
418 if files.len() != entries.len() {
419 return Err(crate::api::Error::transport(
420 None,
421 "vopen returned an invalid result count",
422 ));
423 }
424 let writes: Vec<_> = files
425 .iter()
426 .zip(entries)
427 .map(|(file, (_, data))| crate::api::WriteOp::at(file, 0, data.as_ref()))
428 .collect();
429 let result = self.vwrite(
430 &writes,
431 crate::api::WriteOptions::new().with_write_all(true),
432 );
433 drop(writes);
434 let close_result = self.close_files(files);
435 result?;
436 close_result
437 }
438
439 /// Borrow a standard-I/O adapter with an independent cursor, initially zero.
440 /// Reads/writes use this client's vectors; collecting reads enforce its
441 /// payload budget. The adapter does not own or close the file.
442 fn std_io<'a>(
443 &'a self,
444 file: &'a Self::File,
445 ) -> impl std::io::Read + std::io::Write + std::io::Seek + 'a {
446 super::io::StdIo::new(self, file)
447 }
448
449 /// Synchronize one retained handle through [`Vfsi::vfsync`].
450 fn sync_data(&self, file: &Self::File) -> Result<()> {
451 self.vfsync(&[file], crate::api::SyncMode::Data)
452 }
453
454 /// Synchronize one retained handle's data and metadata through [`Vfsi::vfsync`].
455 fn sync_all(&self, file: &Self::File) -> Result<()> {
456 self.vfsync(&[file], crate::api::SyncMode::All)
457 }
458
459 // Attrs
460 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`].
461 ///
462 /// Query a path following its final symlink; unavailable fields remain None.
463 ///
464 /// To inspect the symlink itself use [`symlink_attrs`](VfsiExt::symlink_attrs).
465 /// To query an opened object after rename, pass [`crate::api::Target::file`].
466 ///
467 /// ```no_run
468 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
469 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
470 /// let attrs = fs.attrs("/file-1")?;
471 /// println!("{:?} bytes; directory={}", attrs.len(), attrs.is_dir());
472 /// # Ok(())
473 /// # }
474 /// ```
475 fn attrs<T: crate::AsTarget<Self::File>>(&self, path: T) -> Result<Attrs> {
476 attrs_query(self, path, crate::api::AttrsOptions::new(), "attrs")
477 }
478
479 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`] with selected fields and follow_final_symlink(false).
480 ///
481 /// Query one path with selected fields and final-symlink behavior; absent fields remain None.
482 ///
483 /// This avoids requesting every optional attribute. Type/size can be
484 /// requested internally even when not selected; missing optional attributes
485 /// must still be handled via their `Option` accessors.
486 ///
487 /// ```no_run
488 /// use vfsi_core::api::{Vfsi, VfsiExt, Attributes, WriteOp};
489 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
490 /// let attrs = fs.attrs_with_options(
491 /// "/file-1", vfsi_core::api::AttrsOptions::new()
492 /// .with_fields(Attributes::MODE | Attributes::BLOCKS).with_follow_final_symlink(false),
493 /// )?;
494 /// if let Some(blocks) = attrs.blocks() {
495 /// println!("allocated blocks: {blocks}");
496 /// }
497 /// # Ok(())
498 /// # }
499 /// ```
500 fn attrs_with_options<T: crate::AsTarget<Self::File>>(
501 &self,
502 path: T,
503 options: crate::api::AttrsOptions,
504 ) -> Result<Attrs> {
505 attrs_query(self, path, options, "attrs_with_options")
506 }
507
508 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`] with follow_final_symlink(false).
509 ///
510 /// Query the final symlink itself instead of following it.
511 ///
512 /// This does not prevent following symlinks in ancestor components and is
513 /// not a race-free namespace confinement primitive.
514 ///
515 /// ```no_run
516 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
517 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
518 /// let attrs = fs.symlink_attrs("/link-or-file")?;
519 /// println!("symlink={}", attrs.is_symlink());
520 /// # Ok(())
521 /// # }
522 /// ```
523 fn symlink_attrs(&self, path: impl AsRef<Path>) -> Result<Attrs> {
524 attrs_query(
525 self,
526 path,
527 crate::api::AttrsOptions::new().with_follow_final_symlink(false),
528 "symlink_attrs",
529 )
530 }
531
532 // Collect directories
533 /// Single-target convenience. For multiple directories, prefer [`VfsiExt::read_dirs_with_options`] or [`VfsiExt::read_dirs`].
534 ///
535 /// Collect one directory using the client's entry and path-byte limits.
536 ///
537 /// ```no_run
538 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
539 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
540 /// for entry in fs.read_dir("/input")? {
541 /// println!("{}", entry.path().display());
542 /// }
543 /// # Ok(())
544 /// # }
545 /// ```
546 fn read_dir(&self, path: impl AsRef<Path>) -> Result<Vec<crate::api::DirEntry>> {
547 self.read_dir_with_options(path, crate::api::ListDirOptions::new())
548 }
549
550 /// Single-target convenience. For multiple directories, prefer [`VfsiExt::read_dirs_with_options`] with one aggregate budget.
551 ///
552 /// Collect one shallow directory with selected metadata and entry/path-byte limits.
553 /// Recursive options are rejected; use `read_dirs_with_options` with
554 /// `ListDirOptions::with_recursive(true)` to collect a tree.
555 ///
556 /// ```no_run
557 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
558 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
559 /// let entries = fs.read_dir_with_options("/input",
560 /// vfsi_core::api::ListDirOptions::new().with_max_entries(100))?;
561 /// println!("{} entries", entries.len());
562 /// # Ok(())
563 /// # }
564 /// ```
565 fn read_dir_with_options(
566 &self,
567 path: impl AsRef<Path>,
568 options: crate::api::ListDirOptions,
569 ) -> Result<Vec<crate::api::DirEntry>> {
570 if options.recursive() {
571 return Err(
572 crate::api::Error::client(0, libc::EINVAL as u32).with_context(
573 "read_dir_with_options requires shallow options",
574 path.as_ref(),
575 ),
576 );
577 }
578 let mut trees = self.read_dirs_with_options(&[path], options)?;
579 Ok(single_completion(single_tree(&mut trees)?, "read_dirs")?.entries)
580 }
581
582 /// Collect directory listings under one aggregate entry/path-byte policy.
583 ///
584 /// There is one listing per input directory, in input order; entry order is
585 /// backend-defined. Entries include metadata, avoiding a separate scalar
586 /// stat per child. The default policy comes from this client's limits.
587 /// Use [`read_dirs_with_options`](VfsiExt::read_dirs_with_options) to select
588 /// attributes, or a visitor instead of collecting large listings.
589 ///
590 /// ```no_run
591 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
592 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
593 /// for listing in fs.read_dirs(&["/input", "/output"])? {
594 /// for entry in listing.entries {
595 /// println!("{}: {:?} bytes", entry.path().display(), entry.attrs().len());
596 /// }
597 /// }
598 /// # Ok(())
599 /// # }
600 /// ```
601 fn read_dirs<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<DirectoryListing>> {
602 let trees = self.read_dirs_with_options(paths, crate::api::ListDirOptions::new())?;
603 if trees.len() != paths.len() || trees.iter().any(|tree| tree.len() != 1) {
604 return Err(crate::api::Error::transport(
605 None,
606 "read_dirs returned an invalid result count",
607 ));
608 }
609 Ok(trees.into_iter().flatten().collect())
610 }
611
612 /// Collect shallow directories or recursive trees, grouped by input root.
613 ///
614 /// `results[i]` contains the listings for `paths[i]`. Shallow mode
615 /// returns exactly one listing per root; recursive mode includes the root
616 /// and descendants. Entries and path-byte budgets are shared across roots;
617 /// recursive collection also charges retained directory paths.
618 /// Collection builds on the batched directory-page visitor.
619 /// Explicit limits override client defaults. Errors discard collected results;
620 /// this is not a snapshot. Symlink entries are not recursively followed.
621 ///
622 /// ```no_run
623 /// use vfsi_core::api::{Vfsi, VfsiExt, Attributes, ListDirOptions};
624 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
625 /// let trees = fs.read_dirs_with_options(&["/input", "/output"],
626 /// ListDirOptions::new().with_recursive(true).with_fields(Attributes::MODE)
627 /// .with_max_entries(10_000).with_max_path_bytes(1024 * 1024))?;
628 /// for tree in trees {
629 /// for listing in tree {
630 /// println!("{}: {} entries", listing.path.display(), listing.entries.len());
631 /// }
632 /// }
633 /// # Ok(())
634 /// # }
635 /// ```
636 fn read_dirs_with_options<P: AsRef<Path>>(
637 &self,
638 paths: &[P],
639 options: crate::api::ListDirOptions,
640 ) -> Result<Vec<Vec<DirectoryListing>>> {
641 let mut trees: Vec<Vec<DirectoryListing>> = (0..paths.len()).map(|_| Vec::new()).collect();
642 let mut positions: Vec<std::collections::HashMap<std::path::PathBuf, usize>> = (0..paths
643 .len())
644 .map(|_| std::collections::HashMap::new())
645 .collect();
646 self.vlistdirs(paths, options, |index, page| {
647 let tree = trees
648 .get_mut(index)
649 .ok_or_else(|| crate::api::Error::transport(None, "invalid visitor root index"))?;
650 let positions = &mut positions[index];
651 if let Some(&slot) = positions.get(&page.path) {
652 tree[slot].entries.extend(page.entries);
653 } else {
654 positions.insert(page.path.clone(), tree.len());
655 // Keep the first page's path and entry storage; later pages
656 // need only a borrowed lookup to find this directory.
657 tree.push(page);
658 }
659 Ok(std::ops::ControlFlow::Continue(()))
660 })?;
661 Ok(trees)
662 }
663
664 // Visit directories
665 /// List one directory through callbacks, optionally recursively.
666 /// For independent roots and owned pages, prefer [`Vfsi::vlistdirs`].
667 ///
668 /// By default, only child `Entry` events are delivered, with no extra root
669 /// attribute request or full-listing allocation on paging backends. Set
670 /// `recursive(true)` to descend without following entry symlinks. Entry-only
671 /// traversal retains native anchored child cursors and sibling batching;
672 /// its order is backend-defined.
673 ///
674 /// `enter_leave(true)` includes the root and emits depth-first `Enter` and
675 /// `Leave` events for traversed directories, including pruned directories.
676 /// In shallow lifecycle mode, children are `Entry` events. Symlinks are
677 /// always `Entry` events. A non-directory lifecycle root produces one Entry.
678 /// Lifecycle traversal currently retains bounded directory buffers because
679 /// the vector page primitive does not expose resumable cursors.
680 /// `sort_by_name(true)` also buffers a bounded directory before sorting.
681 /// Unsorted entry-only visiting remains paged. This is not a snapshot.
682 /// Buffered recursive/lifecycle visiting rejects symlinks in reopened
683 /// directory paths, including ancestors. Backends unable to guarantee that
684 /// return Unsupported instead of checking and then following the path.
685 /// Ordinary shallow listing retains the backend's path resolution; use
686 /// `allow_symlink_components(false)` to require no-follow directory opens explicitly.
687 ///
688 /// The root is depth zero and its children have depth one. Directory depth
689 /// limits control descent; shallow mode ignores them. Lifecycle mode charges
690 /// the root against the entry/path budgets; recursive entry-only mode charges
691 /// retained directory paths. All unspecified budgets inherit client limits.
692 ///
693 /// `SkipSubtree` on Enter prevents reading that directory's children;
694 /// it does nothing on files or Leave. Pruning a recursive directory Entry
695 /// without lifecycle events is rejected: enable `enter_leave(true)`.
696 /// `Stop` returns Stopped immediately,
697 /// without synthesizing pending Leave events. Callback errors propagate
698 /// without replay. Callbacks run outside backend locks and may reenter.
699 ///
700 /// ```no_run
701 /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, WalkControl};
702 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
703 /// fs.listdir("/input", ListDirOptions::new(), |event| {
704 /// println!("{}", event.entry.path().display());
705 /// Ok(WalkControl::Continue)
706 /// })?;
707 /// # Ok(())
708 /// # }
709 /// ```
710 ///
711 /// Recursive lifecycle events allow pruning before listing children:
712 ///
713 /// ```no_run
714 /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, WalkControl, WalkEventKind};
715 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
716 /// fs.listdir("/project",
717 /// vfsi_core::api::WalkOptions::new(ListDirOptions::new().with_recursive(true))
718 /// .with_enter_leave(true).with_sort_by_name(true),
719 /// |event| {
720 /// if event.kind == WalkEventKind::Enter
721 /// && event.entry.file_name() == Some(std::ffi::OsStr::new(".git")) {
722 /// return Ok(WalkControl::SkipSubtree);
723 /// }
724 /// println!("{:?}: {}", event.kind, event.entry.path().display());
725 /// Ok(WalkControl::Continue)
726 /// })?;
727 /// # Ok(())
728 /// # }
729 /// ```
730 fn listdir(
731 &self,
732 root: impl AsRef<Path>,
733 options: impl Into<crate::api::WalkOptions>,
734 visitor: impl FnMut(&crate::api::WalkEvent) -> Result<crate::api::WalkControl>,
735 ) -> Result<crate::api::TraversalCompletion> {
736 super::listdir::listdir(self, root.as_ref(), options.into(), visitor)
737 }
738
739 /// Visit individual entries using the directory-page primitive.
740 /// Empty directories produce pages but no entry callbacks.
741 fn visit_entries_with_options<P: AsRef<Path>>(
742 &self,
743 paths: &[P],
744 options: crate::api::ListDirOptions,
745 mut callback: impl FnMut(usize, &crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
746 ) -> Result<Vec<crate::api::TraversalCompletion>> {
747 self.vlistdirs(paths, options, |index, page| {
748 for entry in &page.entries {
749 if callback(index, entry)?.is_break() {
750 return Ok(std::ops::ControlFlow::Break(()));
751 }
752 }
753 Ok(std::ops::ControlFlow::Continue(()))
754 })
755 }
756
757 /// Visit complete shallow listings in depth-first order with application policy.
758 ///
759 /// `order` compares immutable siblings (for example, locale-aware `ls` ordering).
760 /// The shared workflow sorts them; callbacks cannot replace validated entries.
761 /// `descend` selects directories before their contents are read; it does not
762 /// filter entries delivered in their parent's listing. Symlinks are never
763 /// traversed. Descendant opens reject symlinks in every component; backends
764 /// unable to guarantee that return Unsupported. The root uses the supplied
765 /// path-resolution policy. `visitor` runs outside backend locks and may stop immediately
766 /// or skip all children of the current directory. Errors after delivered
767 /// listings must not trigger replay through another backend.
768 ///
769 /// Storage is bounded by one complete listing and the pending directory
770 /// frontier. Entry/path budgets charge all fetched children, including those
771 /// rejected by `descend`. This ordered workflow deliberately does not
772 /// speculatively list children before admission; use `vlistdirs` to batch
773 /// already-approved independent directories.
774 ///
775 /// ```no_run
776 /// use vfsi_core::api::{Attributes, Result, Vfsi, VfsiExt, WalkControl};
777 /// # fn example(fs: &impl Vfsi) -> Result<()> {
778 /// fs.visit_dirs_ordered("/input",
779 /// fs.limits().walk_options().with_fields(Attributes::MODE | Attributes::SIZE),
780 /// |a, b| a.path().cmp(b.path()),
781 /// |entry| entry.file_name() != Some(std::ffi::OsStr::new(".git")),
782 /// |listing, depth| {
783 /// println!("{}: {} entries at depth {depth}", listing.path.display(), listing.entries.len());
784 /// Ok(WalkControl::Continue)
785 /// })?;
786 /// # Ok(())
787 /// # }
788 /// ```
789 fn visit_dirs_ordered(
790 &self,
791 root: impl AsRef<Path>,
792 options: crate::api::ListDirOptions,
793 mut order: impl FnMut(&crate::api::DirEntry, &crate::api::DirEntry) -> std::cmp::Ordering,
794 mut descend: impl FnMut(&crate::api::DirEntry) -> bool,
795 mut visitor: impl FnMut(DirectoryListing, usize) -> Result<crate::api::WalkControl>,
796 ) -> Result<crate::api::TraversalCompletion> {
797 let options = options.walk_options(self.limits());
798 let root = root.as_ref();
799 let mut budget = crate::internal::TraversalBudget::new(
800 options.max_entries_or(crate::api::DEFAULT_DIRECTORY_MAX_ENTRIES),
801 options.max_path_bytes_or(crate::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES),
802 );
803 budget.charge(root)?;
804 let mut pending = vec![(root.to_path_buf(), 0usize)];
805 while let Some((path, depth)) = pending.pop() {
806 let mut entries = self.read_dir_with_options(
807 &path,
808 budget.remaining_options(
809 crate::api::ListDirOptions::new()
810 .with_fields(options.fields())
811 // Descendants were observed as real directories in a prior
812 // listing. Reopening must not follow swapped ancestors.
813 .with_allow_symlink_components(
814 depth == 0 && options.allow_symlink_components(),
815 ),
816 ),
817 )?;
818 for entry in &entries {
819 budget
820 .charge(entry.path())
821 .map_err(|error| error.with_context("visit_dirs_ordered", entry.path()))?;
822 }
823 entries.sort_by(&mut order);
824 let listing = DirectoryListing { path, entries };
825 let children: Vec<_> = listing
826 .entries
827 .iter()
828 .filter(|entry| entry.attrs().is_dir() && descend(entry))
829 .map(|entry| entry.path().to_path_buf())
830 .collect();
831 match visitor(listing, depth)? {
832 crate::api::WalkControl::Stop => {
833 return Ok(crate::api::TraversalCompletion::Stopped);
834 }
835 crate::api::WalkControl::SkipSubtree => continue,
836 crate::api::WalkControl::Continue => {}
837 }
838 if depth >= options.max_depth_or(Default::default()).get() && !children.is_empty() {
839 if !options.truncate_at_max_depth() {
840 return Err(crate::api::Error::client(0, libc::EFBIG as u32)
841 .with_context("visit_dirs_ordered", &children[0]));
842 }
843 } else {
844 pending.extend(children.into_iter().rev().map(|child| (child, depth + 1)));
845 }
846 }
847 Ok(crate::api::TraversalCompletion::Complete)
848 }
849
850 // Create, move, copy, and remove
851 /// Single-target convenience. For multiple independent directories, prefer [`Vfsi::vmkdir`].
852 ///
853 /// Create one directory; its parent must exist.
854 ///
855 /// An existing entry is an error, even if already a directory. Use
856 /// [`create_dir_all`](VfsiExt::create_dir_all) for missing parents/idempotent
857 /// directory setup, or [`vmkdir`](Vfsi::vmkdir) for independent siblings.
858 ///
859 /// ```no_run
860 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
861 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
862 /// fs.create_dir("/fresh-directory")?;
863 /// # Ok(())
864 /// # }
865 /// ```
866 fn create_dir(&self, path: impl AsRef<Path>) -> Result<()> {
867 self.vmkdir(&[MkDirOp::new(path, 0o777)])
868 }
869
870 /// Truncate or extend one path or opened object using [`Vfsi::vsetattrs`].
871 fn truncate<T: crate::api::AsTarget<Self::File>>(&self, target: T, len: u64) -> Result<()> {
872 self.vsetattrs(&[SetAttrsOp::new(target).len(len)])
873 }
874
875 /// Change permissions on one path or opened object using [`Vfsi::vsetattrs`].
876 fn chmod<T: crate::api::AsTarget<Self::File>>(
877 &self,
878 target: T,
879 permissions: crate::api::Permissions,
880 ) -> Result<()> {
881 self.vsetattrs(&[SetAttrsOp::new(target).permissions(permissions)])
882 }
883
884 /// Change ownership of one path or opened object through [`Vfsi::vsetattrs`].
885 /// `None` leaves the corresponding owner/group unchanged. Use the vector
886 /// with [`SetAttrsOp::with_follow_final_symlink(false)`](SetAttrsOp::with_follow_final_symlink)
887 /// to change a symlink itself.
888 fn chown<T: crate::api::AsTarget<Self::File>>(
889 &self,
890 target: T,
891 uid: Option<u32>,
892 gid: Option<u32>,
893 ) -> Result<()> {
894 let mut op = SetAttrsOp::new(target);
895 if let Some(uid) = uid {
896 op = op.uid(uid);
897 }
898 if let Some(gid) = gid {
899 op = op.gid(gid);
900 }
901 self.vsetattrs(&[op])
902 }
903
904 /// Create one directory with explicit Unix permission bits.
905 fn create_dir_with_mode(&self, path: impl AsRef<Path>, mode: u32) -> Result<()> {
906 self.vmkdir(&[MkDirOp::new(path, mode)])
907 }
908
909 /// Create one symbolic link; submit multiple pairs with [`Vfsi::vsymlink`].
910 fn symlink(&self, target: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
911 self.vsymlink(&[(target, link)])
912 }
913
914 /// Read one symlink target; submit multiple paths with [`Vfsi::vreadlink`].
915 fn read_link(&self, path: impl AsRef<Path>) -> Result<std::path::PathBuf> {
916 let mut targets = self.vreadlink(&[path])?;
917 if targets.len() != 1 {
918 return Err(crate::api::Error::transport(
919 None,
920 "invalid readlink result count",
921 ));
922 }
923 Ok(targets.pop().expect("validated readlink count"))
924 }
925
926 /// Create one hard link; submit multiple pairs with [`Vfsi::vhardlink`].
927 fn hard_link(&self, source: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
928 self.vhardlink(&[(source, link)])
929 }
930
931 /// Single-target convenience. For multiple directory creations, prefer [`Vfsi::vmkdir`]; plan missing parents before their children.
932 ///
933 /// Create missing parents; an error can leave some directories created.
934 ///
935 /// Existing directories are accepted; an existing non-directory component
936 /// is an error. This is not atomic and is not protected against concurrent
937 /// changes to ancestor paths.
938 ///
939 /// ```no_run
940 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
941 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
942 /// fs.create_dir_all("/workspace/results/2026")?;
943 /// # Ok(())
944 /// # }
945 /// ```
946 fn create_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
947 let mut current = std::path::PathBuf::new();
948 for component in path.as_ref().components() {
949 match component {
950 std::path::Component::RootDir => {
951 current.push(Path::new("/"));
952 continue;
953 }
954 std::path::Component::Normal(part) => current.push(part),
955 std::path::Component::CurDir => continue,
956 std::path::Component::ParentDir => {
957 current.push("..");
958 continue;
959 }
960 std::path::Component::Prefix(_) => {
961 return Err(crate::api::Error::client(0, crate::ERR_INVAL));
962 }
963 }
964 match self.vmkdir(&[MkDirOp::new(¤t, 0o777)]) {
965 Ok(()) => {}
966 Err(error) if error.raw_code() == crate::ERR_EXIST => {
967 if !self.attrs(¤t)?.is_dir() {
968 return Err(crate::api::Error::client(0, crate::ERR_NOTDIR)
969 .with_context("create_dir_all", ¤t));
970 }
971 }
972 Err(error) => return Err(error),
973 }
974 }
975 Ok(())
976 }
977
978 /// Single-target convenience. For multiple source/destination pairs, prefer [`Vfsi::vrename`].
979 ///
980 /// Rename within supported namespaces; cross-filesystem moves can fail.
981 ///
982 /// Both paths use this client's namespace. Replacement semantics follow
983 /// the backend/filesystem; this is not a cross-file vector transaction or
984 /// a guarantee of durable directory updates. No copy fallback is implied.
985 ///
986 /// ```no_run
987 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
988 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
989 /// fs.rename("/old-name", "/new-name")?;
990 /// # Ok(())
991 /// # }
992 /// ```
993 fn rename(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
994 self.vrename(&[(source, destination)], crate::api::RenameMode::Replace)
995 }
996
997 /// Single-target convenience. For multiple file pairs, prefer [`Vfsi::vcopy`].
998 ///
999 /// Copy a file's contents; this is not recursive tree copying.
1000 ///
1001 /// Destination creation/replacement and failures follow backend semantics.
1002 /// A failure can leave a partial destination. Do not assume metadata,
1003 /// sparse layout, or durability is preserved. Server-side copy is used
1004 /// only where supported; callers need not build protocol COPY requests.
1005 ///
1006 /// ```no_run
1007 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1008 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1009 /// fs.copy("/source", "/destination")?;
1010 /// # Ok(())
1011 /// # }
1012 /// ```
1013 fn copy(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
1014 self.copy_with_options(source, destination, CopyOptions::default())
1015 }
1016
1017 /// Single-target copy with explicit operation semantics; prefer [`Vfsi::vcopy`]
1018 /// when submitting multiple pairs.
1019 fn copy_with_options(
1020 &self,
1021 source: impl AsRef<Path>,
1022 destination: impl AsRef<Path>,
1023 options: CopyOptions,
1024 ) -> Result<()> {
1025 self.vcopy(&[(source, destination)], options)
1026 }
1027
1028 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API accepts both files and directories.
1029 ///
1030 /// Remove one file or symlink, not the symlink target.
1031 ///
1032 /// Missing paths are errors. To remove a directory use
1033 /// [`remove_dir`](VfsiExt::remove_dir) or [`remove_dir_all`](VfsiExt::remove_dir_all).
1034 ///
1035 /// ```no_run
1036 /// use vfsi_core::api::{Vfsi, VfsiExt, ErrorKind, WriteOp};
1037 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1038 /// match fs.remove_file("/temporary-file") {
1039 /// Ok(()) => (),
1040 /// Err(error) if error.kind() == ErrorKind::NotFound => (),
1041 /// Err(error) => return Err(error),
1042 /// }
1043 /// # Ok(())
1044 /// # }
1045 /// ```
1046 fn remove_file(&self, path: impl AsRef<Path>) -> Result<()> {
1047 let path = path.as_ref();
1048 if self.symlink_attrs(path)?.is_dir() {
1049 return Err(
1050 crate::api::Error::client(0, crate::ERR_ISDIR).with_context("remove_file", path)
1051 );
1052 }
1053 self.vremove(
1054 &[path],
1055 crate::api::RemoveMode::Entry,
1056 crate::api::RemoveOptions::new(),
1057 )
1058 }
1059
1060 /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API does not enforce directory-only inputs.
1061 ///
1062 /// Remove one empty directory; not a recursive operation.
1063 ///
1064 /// Nonempty directories fail. Use [`remove_dir_contents`](VfsiExt::remove_dir_contents)
1065 /// to empty a directory while retaining it, or [`remove_dir_all`](VfsiExt::remove_dir_all)
1066 /// to remove its tree. Only operate on paths whose removal you intend.
1067 ///
1068 /// ```no_run
1069 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1070 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1071 /// fs.remove_dir("/empty-temporary-directory")?;
1072 /// # Ok(())
1073 /// # }
1074 /// ```
1075 fn remove_dir(&self, path: impl AsRef<Path>) -> Result<()> {
1076 let path = path.as_ref();
1077 require_directory(self, path, "remove_dir")?;
1078 self.vremove(
1079 &[path],
1080 crate::api::RemoveMode::Entry,
1081 crate::api::RemoveOptions::new(),
1082 )
1083 }
1084
1085 /// Single-target convenience. For multiple trees, prefer [`Vfsi::vremove`] with `RemoveMode::Tree`.
1086 ///
1087 /// Recursively remove a tree without following directory symlinks.
1088 /// Path-based removal is not a security sandbox or an atomic transaction.
1089 ///
1090 /// A failure can leave a partly removed tree. Use trusted ancestor paths;
1091 /// concurrent namespace substitutions are not a confinement guarantee.
1092 /// This is destructive: do not use it to prepare an arbitrary existing
1093 /// directory for an example or benchmark.
1094 ///
1095 /// ```no_run
1096 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1097 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1098 /// // Remove only the tree that this application intentionally owns.
1099 /// fs.remove_dir_all("/owned-temporary-tree")?;
1100 /// # Ok(())
1101 /// # }
1102 /// ```
1103 fn remove_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
1104 let path = path.as_ref();
1105 require_directory(self, path, "remove_dir_all")?;
1106 self.vremove(
1107 &[path],
1108 crate::api::RemoveMode::Tree,
1109 crate::api::RemoveOptions::new(),
1110 )
1111 }
1112
1113 /// Recursively remove one directory using explicit batching and failure
1114 /// policy. The directory is validated before mutation; symlink roots are
1115 /// not followed. Prefer [`Vfsi::vremove`] when removing several trees.
1116 fn remove_dir_all_with_options(
1117 &self,
1118 path: impl AsRef<Path>,
1119 options: crate::api::RemoveOptions,
1120 ) -> Result<()> {
1121 let path = path.as_ref();
1122 require_directory(self, path, "remove_dir_all")?;
1123 self.vremove(&[path], crate::api::RemoveMode::Tree, options)
1124 }
1125
1126 /// Single-target convenience. For multiple directory roots, prefer [`Vfsi::vremove`].
1127 ///
1128 /// Recursively empty a directory while keeping its root.
1129 ///
1130 /// The root must be a directory, not a symlink. Child symlinks are removed
1131 /// without following their targets. Errors can leave partially removed
1132 /// contents; retaining the root does not make the operation transactional.
1133 /// The path is resolved for this call, not pinned across calls or renames.
1134 /// Use trusted ancestors; this is not a path-confinement guarantee.
1135 ///
1136 /// ```no_run
1137 /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1138 /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1139 /// fs.remove_dir_contents("/owned-scratch-directory")?;
1140 /// // The scratch directory remains available for subsequent work.
1141 /// # Ok(())
1142 /// # }
1143 /// ```
1144 fn remove_dir_contents(&self, path: impl AsRef<Path>) -> Result<()> {
1145 self.remove_dir_contents_with_options(path, crate::api::RemoveOptions::new())
1146 }
1147
1148 /// Recursively empty one directory while retaining its root, using
1149 /// explicit batching and failure policy. The root is validated without
1150 /// following a symlink. Prefer [`Vfsi::vremove`] for several roots.
1151 /// Like [`Self::remove_dir_contents`], this resolves the current pathname;
1152 /// it does not retain a previously opened directory's identity.
1153 fn remove_dir_contents_with_options(
1154 &self,
1155 path: impl AsRef<Path>,
1156 options: crate::api::RemoveOptions,
1157 ) -> Result<()> {
1158 let path = path.as_ref();
1159 require_directory(self, path, "remove_dir_contents")?;
1160 self.vremove(&[path], crate::api::RemoveMode::Contents, options)
1161 }
1162}
1163
1164impl<C: Vfsi + ?Sized> VfsiExt for C {}
1165
1166fn single_completion<T>(mut values: Vec<T>, operation: &'static str) -> Result<T> {
1167 if values.len() != 1 {
1168 return Err(crate::api::Error::transport(
1169 None,
1170 format!("{operation} returned an invalid result count"),
1171 ));
1172 }
1173 Ok(values.remove(0))
1174}
1175fn require_directory<C: Vfsi + ?Sized>(fs: &C, path: &Path, operation: &'static str) -> Result<()> {
1176 if !fs.symlink_attrs(path)?.is_dir() {
1177 return Err(crate::api::Error::client(0, crate::ERR_NOTDIR).with_context(operation, path));
1178 }
1179 Ok(())
1180}
1181fn attrs_query<C: Vfsi + ?Sized>(
1182 client: &C,
1183 target: impl crate::AsTarget<C::File>,
1184 options: crate::api::AttrsOptions,
1185 operation: &'static str,
1186) -> Result<Attrs> {
1187 let target = target.as_target();
1188 let path = match target {
1189 crate::Target::Path(path) => path,
1190 crate::Target::File(file) => file.path(),
1191 };
1192 let results = client
1193 .vgetattrs(&[target], options)
1194 .map_err(|error| error.with_context(operation, path))?;
1195 single_completion(results, "vgetattrs").map_err(|error| error.with_context(operation, path))
1196}
1197
1198fn single_tree(trees: &mut Vec<Vec<DirectoryListing>>) -> Result<Vec<DirectoryListing>> {
1199 if trees.len() != 1 {
1200 return Err(crate::api::Error::transport(
1201 None,
1202 "read_dirs returned an invalid root count",
1203 ));
1204 }
1205 Ok(trees.remove(0))
1206}