Skip to main content

vfsi_sync/
client.rs

1//! Owned, shareable synchronous client and file handles.
2
3use std::fmt;
4use std::io::{self, Read, Seek, SeekFrom as IoSeekFrom, Write};
5use std::path::{Component, Path, PathBuf};
6use std::sync::{Arc, Mutex};
7use std::time::SystemTime;
8
9use crate::traits::{validate_read_into_results, validate_read_results, validate_write_results};
10use crate::{
11    AttrMask, Capabilities, CopyFileSystem, DEFAULT_READ_MAX_BYTES, DirEntry, DirectoryFileSystem,
12    FileSystem, LinkFileSystem, Metadata, MetadataFileSystem, MetadataQuery, MetadataUpdate,
13    NamespaceFileSystem, OpenFlags, OpenRequest, Permissions, ReadDirOptions, ReadOp, ReadResult,
14    ReadStreamOptions, SetAttributes, VecFs, VectorFileSystem, VfDir, VfError, VfFile, VfOffset,
15    VfResult, WriteOpRef, WriteResult,
16};
17
18fn io_error(error: VfError) -> io::Error {
19    error.into()
20}
21
22fn poisoned() -> VfError {
23    VfError::client(0, crate::ERR_IO)
24}
25
26fn wrong_result_count(operation: &str, expected: usize, actual: usize) -> VfError {
27    VfError::transport(
28        None,
29        format!("{operation} backend returned {actual} results for {expected} requests"),
30    )
31}
32
33/// Cloneable owner of one synchronous backend connection.
34pub struct FsClient<F> {
35    inner: Arc<Mutex<F>>,
36}
37
38impl<F> Clone for FsClient<F> {
39    fn clone(&self) -> Self {
40        Self {
41            inner: Arc::clone(&self.inner),
42        }
43    }
44}
45
46impl<F> fmt::Debug for FsClient<F> {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        formatter.debug_struct("FsClient").finish_non_exhaustive()
49    }
50}
51
52impl<F> FsClient<F> {
53    pub fn new(filesystem: F) -> Self {
54        Self {
55            inner: Arc::new(Mutex::new(filesystem)),
56        }
57    }
58
59    fn lock(&self) -> VfResult<std::sync::MutexGuard<'_, F>> {
60        self.inner.lock().map_err(|_| poisoned())
61    }
62
63    pub fn into_inner(self) -> Result<F, Self> {
64        match Arc::try_unwrap(self.inner) {
65            Ok(mutex) => match mutex.into_inner() {
66                Ok(filesystem) => Ok(filesystem),
67                Err(poisoned) => Ok(poisoned.into_inner()),
68            },
69            Err(inner) => Err(Self { inner }),
70        }
71    }
72}
73
74impl<F: FileSystem> FsClient<F> {
75    /// Open a path read-only.
76    pub fn open(&self, path: impl AsRef<Path>) -> VfResult<FsFile<F>> {
77        self.open_with(OpenRequest::new(path.as_ref(), OpenFlags::READ))
78    }
79
80    pub fn open_with(&self, request: OpenRequest) -> VfResult<FsFile<F>> {
81        let file = self.lock()?.open_one(&request)?;
82        Ok(FsFile {
83            inner: Arc::clone(&self.inner),
84            file: Some(file),
85            path: request.path,
86        })
87    }
88
89    pub fn open_options(&self) -> OpenOptions<'_, F> {
90        OpenOptions::new(self)
91    }
92
93    pub fn create(&self, path: impl AsRef<Path>) -> VfResult<FsFile<F>> {
94        self.open_with(OpenRequest::new(
95            path.as_ref(),
96            OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,
97        ))
98    }
99
100    pub fn read(&self, path: impl AsRef<Path>) -> VfResult<Vec<u8>> {
101        self.read_with_limit(path, DEFAULT_READ_MAX_BYTES)
102    }
103
104    /// Read one complete file while limiting the returned allocation.
105    ///
106    /// Use [`FsFile::read_native`] or [`std::io::Read`] to stream files that
107    /// should not be held in one allocation.
108    pub fn read_with_limit(&self, path: impl AsRef<Path>, max_bytes: usize) -> VfResult<Vec<u8>> {
109        let path = path.as_ref();
110        let mut file = self.open(path)?;
111        let mut output = Vec::new();
112        let mut chunk = vec![0; 64 * 1024];
113        loop {
114            let remaining = max_bytes.saturating_sub(output.len());
115            let request = if remaining == 0 {
116                1
117            } else {
118                remaining.min(chunk.len())
119            };
120            let read = file.read_native(&mut chunk[..request])?;
121            if read == 0 {
122                break;
123            }
124            if read > remaining {
125                return Err(VfError::failure(0, libc::EFBIG as u32).with_context("read", path));
126            }
127            output.extend_from_slice(&chunk[..read]);
128        }
129        file.close()?;
130        Ok(output)
131    }
132
133    pub fn read_to_string(&self, path: impl AsRef<Path>) -> VfResult<String> {
134        self.read_to_string_with_limit(path, DEFAULT_READ_MAX_BYTES)
135    }
136
137    /// Read one complete UTF-8 file with a caller-selected allocation limit.
138    pub fn read_to_string_with_limit(
139        &self,
140        path: impl AsRef<Path>,
141        max_bytes: usize,
142    ) -> VfResult<String> {
143        let path = path.as_ref();
144        String::from_utf8(self.read_with_limit(path, max_bytes)?)
145            .map_err(|_| VfError::client(0, crate::ERR_INVAL).with_context("read_to_string", path))
146    }
147
148    pub fn write(&self, path: impl AsRef<Path>, data: &[u8]) -> VfResult<()> {
149        let path = path.as_ref();
150        let mut file = self.create(path)?;
151        let mut written = 0;
152        while written < data.len() {
153            let count = file.write_native(&data[written..])?;
154            if count == 0 {
155                return Err(VfError::client(0, crate::ERR_IO).with_context("write", path));
156            }
157            written += count;
158        }
159        file.close()
160    }
161}
162
163impl<F: FileSystem> FsClient<F> {
164    /// Stream one file from offset zero in bounded chunks.
165    ///
166    /// The callback runs without holding the backend lock, so it may use this
167    /// client or drop other files owned by it. Return `Ok(false)` to stop
168    /// successfully. Callback errors
169    /// are propagated. The file is closed on success, cancellation, callback
170    /// error, or read error. At most one requested chunk is buffered at once.
171    pub fn read_stream(
172        &self,
173        path: impl AsRef<Path>,
174        callback: impl FnMut(u64, &[u8]) -> VfResult<bool>,
175    ) -> VfResult<()> {
176        self.read_stream_with_options(path, ReadStreamOptions::default(), callback)
177    }
178
179    /// Stream one file using an explicit maximum chunk size.
180    pub fn read_stream_with_options(
181        &self,
182        path: impl AsRef<Path>,
183        options: ReadStreamOptions,
184        mut callback: impl FnMut(u64, &[u8]) -> VfResult<bool>,
185    ) -> VfResult<()> {
186        let chunk_size = options.chunk_size_bytes();
187        if chunk_size == 0 {
188            return Err(VfError::failure(0, crate::ERR_INVAL));
189        }
190
191        let file = self.open(path)?;
192        let raw_file = file.raw()?.clone();
193        let operation = (|| -> VfResult<()> {
194            let mut offset = 0u64;
195            loop {
196                let request = ReadOp::at(raw_file.clone(), offset, chunk_size);
197                let result = {
198                    let mut backend = self.lock()?;
199                    let result = backend.read_one(&request)?;
200                    validate_read_results(
201                        "read_stream",
202                        std::slice::from_ref(&request),
203                        std::slice::from_ref(&result),
204                    )?;
205                    result
206                };
207                let length = result.data.len();
208                if !result.data.is_empty() && !callback(offset, &result.data)? {
209                    return Ok(());
210                }
211                if result.eof {
212                    return Ok(());
213                }
214                offset = offset
215                    .checked_add(length as u64)
216                    .ok_or_else(|| VfError::client(0, libc::EOVERFLOW as u32))?;
217            }
218        })();
219        let close = file.close();
220        match operation {
221            Err(error) => Err(error),
222            Ok(()) => close,
223        }
224    }
225}
226
227impl<F: MetadataFileSystem> FsClient<F> {
228    pub fn metadata(&self, path: impl AsRef<Path>) -> VfResult<Metadata> {
229        self.lock()?.metadata_path(path.as_ref(), true)
230    }
231
232    pub fn symlink_metadata(&self, path: impl AsRef<Path>) -> VfResult<Metadata> {
233        self.lock()?.metadata_path(path.as_ref(), false)
234    }
235
236    pub fn set_metadata(&self, path: impl AsRef<Path>) -> SetMetadata<'_, F> {
237        SetMetadata {
238            client: self,
239            path: path.as_ref().to_path_buf(),
240            update: MetadataUpdate::new(),
241            follow: true,
242        }
243    }
244}
245
246impl<F: DirectoryFileSystem> FsClient<F> {
247    pub fn create_dir(&self, path: impl AsRef<Path>) -> VfResult<()> {
248        self.create_dir_with_mode(path, 0o777)
249    }
250
251    pub fn create_dir_with_mode(&self, path: impl AsRef<Path>, mode: u32) -> VfResult<()> {
252        self.lock()?.create_dir_one(path.as_ref(), mode)
253    }
254
255    pub fn read_dir(&self, path: impl AsRef<Path>) -> VfResult<Vec<DirEntry>> {
256        self.read_dir_with_options(path, ReadDirOptions::default())
257    }
258
259    /// Read one directory with explicit entry and path-storage limits.
260    pub fn read_dir_with_options(
261        &self,
262        path: impl AsRef<Path>,
263        options: ReadDirOptions,
264    ) -> VfResult<Vec<DirEntry>> {
265        self.lock()?.read_dir_one(path.as_ref(), options)
266    }
267}
268
269impl<F: DirectoryFileSystem + MetadataFileSystem> FsClient<F> {
270    pub fn create_dir_all(&self, path: impl AsRef<Path>) -> VfResult<()> {
271        let path = path.as_ref();
272        let mut current = PathBuf::new();
273        for component in path.components() {
274            match component {
275                Component::RootDir => {
276                    current.push(Path::new("/"));
277                    continue;
278                }
279                Component::Normal(part) => current.push(part),
280                Component::CurDir => continue,
281                Component::ParentDir => {
282                    // Preserve `..` so the backend resolves it relative to
283                    // its own rooted namespace. Lexically popping `/` would
284                    // accidentally turn a later absolute component into a
285                    // current-directory-relative path.
286                    current.push("..");
287                    continue;
288                }
289                Component::Prefix(_) => return Err(VfError::client(0, crate::ERR_INVAL)),
290            }
291            match self.create_dir(&current) {
292                Ok(()) => {}
293                Err(error) if error.err_no() == crate::ERR_EXIST => {
294                    if !self.metadata(&current)?.is_dir() {
295                        return Err(VfError::client(0, crate::ERR_NOTDIR)
296                            .with_context("create_dir_all", &current));
297                    }
298                }
299                Err(error) => return Err(error),
300            }
301        }
302        Ok(())
303    }
304}
305
306impl<F: NamespaceFileSystem + MetadataFileSystem> FsClient<F> {
307    fn removal_metadata(&self, path: &Path, operation: &'static str) -> VfResult<Metadata> {
308        let mut filesystem = self.lock()?;
309        let follow = !filesystem.capabilities().contains(Capabilities::LSTAT);
310        filesystem
311            .metadata_path(path, follow)
312            .map_err(|error| error.with_context(operation, path))
313    }
314
315    pub fn remove_file(&self, path: impl AsRef<Path>) -> VfResult<()> {
316        let path = path.as_ref();
317        if self.removal_metadata(path, "remove_file")?.is_dir() {
318            return Err(VfError::client(0, crate::ERR_ISDIR).with_context("remove_file", path));
319        }
320        self.lock()?.remove_one(path, false)
321    }
322
323    pub fn remove_dir(&self, path: impl AsRef<Path>) -> VfResult<()> {
324        let path = path.as_ref();
325        if !self.removal_metadata(path, "remove_dir")?.is_dir() {
326            return Err(VfError::client(0, crate::ERR_NOTDIR).with_context("remove_dir", path));
327        }
328        self.lock()?.remove_one(path, false)
329    }
330
331    pub fn remove_dir_all(&self, path: impl AsRef<Path>) -> VfResult<()> {
332        let path = path.as_ref();
333        if !self.removal_metadata(path, "remove_dir_all")?.is_dir() {
334            return Err(VfError::client(0, crate::ERR_NOTDIR).with_context("remove_dir_all", path));
335        }
336        self.lock()?.remove_one(path, true)
337    }
338
339    /// Remove the contents of a directory, keeping the directory itself.
340    pub fn remove_dir_contents(&self, path: impl AsRef<Path>) -> VfResult<()> {
341        let path = path.as_ref();
342        if !self.removal_metadata(path, "remove_dir_contents")?.is_dir() {
343            return Err(
344                VfError::client(0, crate::ERR_NOTDIR).with_context("remove_dir_contents", path)
345            );
346        }
347        self.lock()?.remove_dir_contents(path)
348    }
349
350    pub fn rename(&self, from: impl AsRef<Path>, to: impl AsRef<Path>) -> VfResult<()> {
351        self.lock()?.rename_one(from.as_ref(), to.as_ref())
352    }
353}
354
355impl<F: VecFs> FsClient<F> {
356    /// Create `path` if missing, otherwise empty it. Errors if it exists and is
357    /// not a directory (a symlink to a directory is not a directory here).
358    pub fn ensure_empty_dir(&self, path: impl AsRef<Path>) -> VfResult<()> {
359        self.lock()?.ensure_empty_dir(path.as_ref())
360    }
361
362    /// Open a directory for handle-rooted removal. The returned handle cannot
363    /// be swapped for a symlink between opening and removal; release it with
364    /// [`close_dir`](Self::close_dir).
365    pub fn open_dir(&self, path: impl AsRef<Path>) -> VfResult<VfDir> {
366        self.lock()?.open_dir(path.as_ref())
367    }
368
369    /// Remove the contents of a directory handle, keeping the directory.
370    pub fn remove_dir_contents_handle(&self, dir: &VfDir) -> VfResult<()> {
371        self.lock()?.rm_dir_contents(dir)
372    }
373
374    /// Release a handle from [`open_dir`](Self::open_dir).
375    pub fn close_dir(&self, dir: &VfDir) -> VfResult<()> {
376        self.lock()?.close_dir(dir)
377    }
378}
379
380impl<F: LinkFileSystem> FsClient<F> {
381    pub fn symlink(&self, target: impl AsRef<Path>, link: impl AsRef<Path>) -> VfResult<()> {
382        self.lock()?.symlink_one(target.as_ref(), link.as_ref())
383    }
384
385    pub fn hard_link(&self, source: impl AsRef<Path>, link: impl AsRef<Path>) -> VfResult<()> {
386        self.lock()?.hard_link_one(source.as_ref(), link.as_ref())
387    }
388
389    pub fn read_link(&self, path: impl AsRef<Path>) -> VfResult<PathBuf> {
390        self.lock()?.read_link_one(path.as_ref())
391    }
392}
393
394impl<F: CopyFileSystem> FsClient<F> {
395    pub fn copy(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> VfResult<()> {
396        self.lock()?.copy_one(source.as_ref(), destination.as_ref())
397    }
398}
399
400impl<F: VecFs> FsClient<F> {
401    /// Fetch no-follow metadata for many paths using the backend's vector
402    /// operation. Useful for routing without one metadata RPC per path.
403    pub fn symlink_metadatav(&self, paths: &[&Path]) -> VfResult<Vec<Metadata>> {
404        let mut attrs: Vec<_> = paths
405            .iter()
406            .map(|path| crate::VfAttrs {
407                file: VfFile::from_os_path(path),
408                masks: AttrMask::MODE,
409                ..crate::VfAttrs::default()
410            })
411            .collect();
412        self.lock()?.lgetattrsv(&mut attrs).map_err(|error| {
413            error
414                .index_opt()
415                .and_then(|index| paths.get(index))
416                .map_or(error.clone(), |path| {
417                    error.with_context("symlink_metadatav", path)
418                })
419        })?;
420        Ok(attrs.into_iter().map(Metadata::from).collect())
421    }
422}
423
424impl<F: VectorFileSystem> FsClient<F> {
425    /// Open an ordered vector of files.
426    ///
427    /// Success returns one RAII handle per request. Failure returns no
428    /// handles; VFSI does not promise transactional rollback of other
429    /// filesystem effects such as file creation.
430    pub fn openv(&self, requests: &[OpenRequest]) -> VfResult<Vec<FsFile<F>>> {
431        let mut filesystem = self.lock()?;
432        let files = filesystem.open_many(requests).map_err(|error| {
433            error
434                .index_opt()
435                .and_then(|index| requests.get(index))
436                .map_or(error.clone(), |request| {
437                    error.with_context("openv", &request.path)
438                })
439        })?;
440        if files.len() != requests.len() {
441            let error = wrong_result_count("openv", requests.len(), files.len());
442            for file in &files {
443                let _ = filesystem.close_one(file);
444            }
445            return Err(error);
446        }
447        drop(filesystem);
448        Ok(files
449            .into_iter()
450            .zip(requests)
451            .map(|(file, request)| FsFile {
452                inner: Arc::clone(&self.inner),
453                file: Some(file),
454                path: request.path.clone(),
455            })
456            .collect())
457    }
458
459    /// Close a group of files through one vector operation.
460    ///
461    /// On failure, each handle remains armed for best-effort cleanup on drop;
462    /// explicitly closed prefix handles may consequently receive a harmless
463    /// second close attempt.
464    pub fn closev(&self, mut files: Vec<FsFile<F>>) -> VfResult<()> {
465        for (index, file) in files.iter().enumerate() {
466            self.validate_owner(file, index)?;
467        }
468        let descriptors: Vec<VfFile> = files
469            .iter()
470            .map(|file| file.raw().cloned())
471            .collect::<VfResult<_>>()?;
472        self.lock()?.close_many(&descriptors).map_err(|error| {
473            error
474                .index_opt()
475                .and_then(|index| files.get(index))
476                .map_or(error.clone(), |file| {
477                    error.with_context("closev", file.path())
478                })
479        })?;
480        for file in &mut files {
481            file.file = None;
482        }
483        Ok(())
484    }
485
486    pub fn readv(&self, requests: &[FsRead<'_, F>]) -> VfResult<Vec<ReadResult>> {
487        let reads = self.read_ops(requests)?;
488        let results = self.lock()?.read_many(&reads).map_err(|error| {
489            error
490                .index_opt()
491                .and_then(|index| requests.get(index))
492                .map_or(error.clone(), |request| {
493                    error.with_context("readv", request.file.path())
494                })
495        })?;
496        if results.len() != requests.len() {
497            return Err(wrong_result_count("readv", requests.len(), results.len()));
498        }
499        validate_read_results("readv", &reads, &results).map_err(|error| {
500            error
501                .index_opt()
502                .and_then(|index| requests.get(index))
503                .map_or(error.clone(), |request| {
504                    error.with_context("readv", request.file.path())
505                })
506        })?;
507        Ok(results)
508    }
509
510    fn read_ops(&self, requests: &[FsRead<'_, F>]) -> VfResult<Vec<ReadOp>> {
511        let mut reads = Vec::with_capacity(requests.len());
512        for (index, request) in requests.iter().enumerate() {
513            self.validate_owner(request.file, index)?;
514            reads.push(ReadOp::new(
515                request.file.raw()?.clone(),
516                request.offset,
517                request.length,
518            ));
519        }
520        Ok(reads)
521    }
522
523    pub fn readv_into(&self, requests: &mut [FsReadInto<'_, F>]) -> VfResult<Vec<usize>> {
524        let mut reads = Vec::with_capacity(requests.len());
525        for (index, request) in requests.iter().enumerate() {
526            self.validate_owner(request.file, index)?;
527            reads.push(ReadOp::new(
528                request.file.raw()?.clone(),
529                request.offset,
530                request.buffer.len(),
531            ));
532        }
533        let results = {
534            let mut buffers: Vec<&mut [u8]> = requests
535                .iter_mut()
536                .map(|request| &mut *request.buffer)
537                .collect();
538            self.lock()?.read_many_into(&reads, &mut buffers)
539        }
540        .map_err(|error| {
541            error
542                .index_opt()
543                .and_then(|index| requests.get(index))
544                .map_or(error.clone(), |request| {
545                    error.with_context("readv_into", request.file.path())
546                })
547        })?;
548        validate_read_into_results("readv_into", &reads, &results).map_err(|error| {
549            error
550                .index_opt()
551                .and_then(|index| requests.get(index))
552                .map_or(error.clone(), |request| {
553                    error.with_context("readv_into", request.file.path())
554                })
555        })?;
556        Ok(results.into_iter().map(|result| result.read).collect())
557    }
558
559    pub fn writev(&self, requests: &[FsWrite<'_, F>]) -> VfResult<Vec<WriteResult>> {
560        let writes = self.write_ops(requests)?;
561        let results = self.lock()?.write_many(&writes).map_err(|error| {
562            error
563                .index_opt()
564                .and_then(|index| requests.get(index))
565                .map_or(error.clone(), |request| {
566                    error.with_context("writev", request.file.path())
567                })
568        })?;
569        if results.len() != requests.len() {
570            return Err(wrong_result_count("writev", requests.len(), results.len()));
571        }
572        validate_write_results("writev", &writes, &results).map_err(|error| {
573            error
574                .index_opt()
575                .and_then(|index| requests.get(index))
576                .map_or(error.clone(), |request| {
577                    error.with_context("writev", request.file.path())
578                })
579        })?;
580        Ok(results)
581    }
582
583    fn write_ops<'a>(&self, requests: &'a [FsWrite<'a, F>]) -> VfResult<Vec<WriteOpRef<'a>>> {
584        let mut writes = Vec::with_capacity(requests.len());
585        for (index, request) in requests.iter().enumerate() {
586            self.validate_owner(request.file, index)?;
587            writes.push(WriteOpRef::new(
588                request.file.raw()?,
589                request.offset,
590                request.data,
591            ));
592        }
593        Ok(writes)
594    }
595
596    fn validate_owner(&self, file: &FsFile<F>, index: usize) -> VfResult<()> {
597        if Arc::ptr_eq(&self.inner, &file.inner) {
598            Ok(())
599        } else {
600            Err(VfError::client(index, crate::ERR_INVAL))
601        }
602    }
603}
604
605/// `std::fs::OpenOptions`-style builder tied to an [`FsClient`].
606pub struct OpenOptions<'a, F: FileSystem> {
607    client: &'a FsClient<F>,
608    flags: OpenFlags,
609    mode: u32,
610}
611
612impl<F: FileSystem> Clone for OpenOptions<'_, F> {
613    fn clone(&self) -> Self {
614        Self {
615            client: self.client,
616            flags: self.flags,
617            mode: self.mode,
618        }
619    }
620}
621
622impl<F: FileSystem> fmt::Debug for OpenOptions<'_, F> {
623    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
624        formatter
625            .debug_struct("OpenOptions")
626            .field("flags", &self.flags)
627            .field("mode", &format_args!("{:#o}", self.mode))
628            .finish()
629    }
630}
631
632impl<'a, F: FileSystem> OpenOptions<'a, F> {
633    fn new(client: &'a FsClient<F>) -> Self {
634        Self {
635            client,
636            flags: OpenFlags::empty(),
637            mode: 0o666,
638        }
639    }
640
641    pub fn read(&mut self, enabled: bool) -> &mut Self {
642        self.flags.set(OpenFlags::READ, enabled);
643        self
644    }
645
646    pub fn write(&mut self, enabled: bool) -> &mut Self {
647        self.flags.set(OpenFlags::WRITE, enabled);
648        self
649    }
650
651    pub fn append(&mut self, enabled: bool) -> &mut Self {
652        self.flags.set(OpenFlags::APPEND, enabled);
653        self
654    }
655
656    pub fn truncate(&mut self, enabled: bool) -> &mut Self {
657        self.flags.set(OpenFlags::TRUNCATE, enabled);
658        self
659    }
660
661    pub fn create(&mut self, enabled: bool) -> &mut Self {
662        self.flags.set(OpenFlags::CREATE, enabled);
663        self
664    }
665
666    pub fn create_new(&mut self, enabled: bool) -> &mut Self {
667        self.flags.set(OpenFlags::CREATE_NEW, enabled);
668        self
669    }
670
671    pub fn mode(&mut self, mode: u32) -> &mut Self {
672        self.mode = mode;
673        self
674    }
675
676    pub fn open(&self, path: impl AsRef<Path>) -> VfResult<FsFile<F>> {
677        self.client
678            .open_with(OpenRequest::new(path.as_ref(), self.flags).mode(self.mode))
679    }
680}
681
682impl<F: VectorFileSystem> OpenOptions<'_, F> {
683    pub fn openv<P: AsRef<Path>>(&self, paths: &[P]) -> VfResult<Vec<FsFile<F>>> {
684        let requests: Vec<OpenRequest> = paths
685            .iter()
686            .map(|path| OpenRequest::new(path.as_ref(), self.flags).mode(self.mode))
687            .collect();
688        self.client.openv(&requests)
689    }
690}
691
692/// Builder for an atomic metadata update request.
693pub struct SetMetadata<'a, F: MetadataFileSystem> {
694    client: &'a FsClient<F>,
695    path: PathBuf,
696    update: MetadataUpdate,
697    follow: bool,
698}
699
700impl<F: MetadataFileSystem> Clone for SetMetadata<'_, F> {
701    fn clone(&self) -> Self {
702        Self {
703            client: self.client,
704            path: self.path.clone(),
705            update: self.update.clone(),
706            follow: self.follow,
707        }
708    }
709}
710
711impl<F: MetadataFileSystem> fmt::Debug for SetMetadata<'_, F> {
712    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
713        formatter
714            .debug_struct("SetMetadata")
715            .field("path", &self.path)
716            .field("update", &self.update)
717            .field("follow", &self.follow)
718            .finish_non_exhaustive()
719    }
720}
721
722impl<F: MetadataFileSystem> SetMetadata<'_, F> {
723    pub fn permissions(&mut self, permissions: Permissions) -> &mut Self {
724        self.update.permissions = Some(permissions);
725        self
726    }
727
728    pub fn len(&mut self, len: u64) -> &mut Self {
729        self.update.len = Some(len);
730        self
731    }
732
733    pub fn accessed(&mut self, accessed: SystemTime) -> &mut Self {
734        self.update.accessed = Some(accessed);
735        self
736    }
737
738    pub fn modified(&mut self, modified: SystemTime) -> &mut Self {
739        self.update.modified = Some(modified);
740        self
741    }
742
743    pub fn follow_symlinks(&mut self, follow: bool) -> &mut Self {
744        self.follow = follow;
745        self
746    }
747
748    pub fn apply(&self) -> VfResult<()> {
749        self.client
750            .lock()?
751            .set_metadata_path(&self.path, self.update.clone(), self.follow)
752    }
753}
754
755/// Owned RAII file which does not borrow the client.
756///
757/// Dropping an open file attempts a best-effort CLOSE. This can block on the
758/// backend mutex and on network I/O; use [`FsFile::try_close`] when the close
759/// result matters or when its timing must be controlled.
760pub struct FsFile<F: FileSystem> {
761    inner: Arc<Mutex<F>>,
762    file: Option<VfFile>,
763    path: PathBuf,
764}
765
766impl<F: FileSystem> fmt::Debug for FsFile<F> {
767    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
768        formatter
769            .debug_struct("FsFile")
770            .field("path", &self.path)
771            .field("is_open", &self.file.is_some())
772            .finish()
773    }
774}
775
776impl<F: FileSystem> FsFile<F> {
777    fn raw(&self) -> VfResult<&VfFile> {
778        self.file
779            .as_ref()
780            .ok_or_else(|| VfError::client(0, crate::ERR_EBADF))
781    }
782
783    /// Positional read which does not alter the file cursor.
784    pub fn read_at(&self, buffer: &mut [u8], offset: u64) -> VfResult<usize> {
785        self.read_into(buffer, VfOffset::At(offset))
786    }
787
788    /// Positional write which does not alter the file cursor.
789    pub fn write_at(&self, buffer: &[u8], offset: u64) -> VfResult<usize> {
790        self.write_from(buffer, VfOffset::At(offset))
791    }
792
793    pub fn read_request_at(&self, offset: u64, length: usize) -> FsRead<'_, F> {
794        FsRead {
795            file: self,
796            offset: VfOffset::At(offset),
797            length,
798        }
799    }
800
801    pub fn path(&self) -> &Path {
802        &self.path
803    }
804
805    /// Query metadata for the open object without resolving its path again.
806    pub fn metadata(&self) -> VfResult<Metadata> {
807        let attributes = AttrMask::MODE
808            | AttrMask::SIZE
809            | AttrMask::NLINK
810            | AttrMask::FILEID
811            | AttrMask::UID
812            | AttrMask::GID
813            | AttrMask::ATIME
814            | AttrMask::MTIME
815            | AttrMask::CTIME
816            | AttrMask::CHANGE;
817        self.inner
818            .lock()
819            .map_err(|_| poisoned())?
820            .metadata(MetadataQuery::new(self.raw()?.clone(), attributes))
821            .map(Metadata::from)
822            .map_err(|error| error.with_context("metadata", &self.path))
823    }
824
825    /// Truncate or extend the open file.
826    pub fn set_len(&self, len: u64) -> VfResult<()> {
827        let mut update = SetAttributes::new(self.raw()?.clone());
828        update.size = Some(len);
829        self.inner
830            .lock()
831            .map_err(|_| poisoned())?
832            .set_attributes(update)
833            .map_err(|error| error.with_context("set_len", &self.path))
834    }
835
836    /// Change permissions on the open file.
837    pub fn set_permissions(&self, permissions: Permissions) -> VfResult<()> {
838        let mut update = SetAttributes::new(self.raw()?.clone());
839        update.mode = Some(permissions.mode());
840        self.inner
841            .lock()
842            .map_err(|_| poisoned())?
843            .set_attributes(update)
844            .map_err(|error| error.with_context("set_permissions", &self.path))
845    }
846
847    pub fn write_request_at<'a>(&'a self, offset: u64, data: &'a [u8]) -> FsWrite<'a, F> {
848        FsWrite {
849            file: self,
850            offset: VfOffset::At(offset),
851            data,
852        }
853    }
854
855    pub fn read_request_at_into<'a>(
856        &'a self,
857        offset: u64,
858        buffer: &'a mut [u8],
859    ) -> FsReadInto<'a, F> {
860        FsReadInto {
861            file: self,
862            offset: VfOffset::At(offset),
863            buffer,
864        }
865    }
866
867    pub fn read_native(&mut self, buffer: &mut [u8]) -> VfResult<usize> {
868        self.read_into(buffer, VfOffset::Cur)
869    }
870
871    pub fn write_native(&mut self, buffer: &[u8]) -> VfResult<usize> {
872        self.write_from(buffer, VfOffset::Cur)
873    }
874
875    fn read_into(&self, buffer: &mut [u8], offset: VfOffset) -> VfResult<usize> {
876        if buffer.is_empty() {
877            return Ok(0);
878        }
879        let request = ReadOp::new(self.raw()?.clone(), offset, buffer.len());
880        let result = self
881            .inner
882            .lock()
883            .map_err(|_| poisoned())?
884            .read_one_into(&request, buffer)
885            .map_err(|error| error.with_context("read", &self.path))?;
886        validate_read_into_results(
887            "read",
888            std::slice::from_ref(&request),
889            std::slice::from_ref(&result),
890        )
891        .map_err(|error| error.with_context("read", &self.path))?;
892        Ok(result.read)
893    }
894
895    fn write_from(&self, buffer: &[u8], offset: VfOffset) -> VfResult<usize> {
896        if buffer.is_empty() {
897            return Ok(0);
898        }
899        let file = self.raw()?.clone();
900        let result = self
901            .inner
902            .lock()
903            .map_err(|_| poisoned())?
904            .write_one(WriteOpRef::new(&file, offset, buffer))
905            .map_err(|error| error.with_context("write", &self.path))?;
906        if result.written > buffer.len() {
907            return Err(VfError::client(0, crate::ERR_IO).with_context("write", &self.path));
908        }
909        Ok(result.written)
910    }
911
912    pub fn sync_data(&self) -> VfResult<()> {
913        self.inner
914            .lock()
915            .map_err(|_| poisoned())?
916            .sync_data(self.raw()?)
917            .map_err(|error| error.with_context("sync_data", &self.path))
918    }
919
920    pub fn sync_all(&self) -> VfResult<()> {
921        self.inner
922            .lock()
923            .map_err(|_| poisoned())?
924            .sync_all(self.raw()?)
925            .map_err(|error| error.with_context("sync_all", &self.path))
926    }
927
928    /// Attempt to close without consuming the handle. A failed close leaves
929    /// the handle armed so the caller can reconcile or retry it explicitly.
930    pub fn try_close(&mut self) -> VfResult<()> {
931        let Some(file) = self.file.as_ref() else {
932            return Ok(());
933        };
934        self.inner
935            .lock()
936            .map_err(|_| poisoned())?
937            .close_one(file)
938            .map_err(|error| error.with_context("close", &self.path))?;
939        self.file = None;
940        Ok(())
941    }
942
943    /// Consume and close the handle. On failure, `Drop` makes one best-effort
944    /// cleanup attempt; use [`try_close`](Self::try_close) to retain control.
945    pub fn close(mut self) -> VfResult<()> {
946        self.try_close()
947    }
948
949    /// Seek while retaining [`VfError`] protocol and path information.
950    pub fn seek_native(&mut self, position: IoSeekFrom) -> VfResult<u64> {
951        self.inner
952            .lock()
953            .map_err(|_| poisoned())?
954            .seek_one(self.raw()?, position)
955            .map_err(|error| error.with_context("seek", &self.path))
956    }
957}
958
959impl<F: FileSystem> Read for FsFile<F> {
960    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
961        self.read_native(buffer).map_err(io_error)
962    }
963}
964
965impl<F: FileSystem> Write for FsFile<F> {
966    fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
967        self.write_native(buffer).map_err(io_error)
968    }
969
970    fn flush(&mut self) -> io::Result<()> {
971        self.sync_data().map_err(io_error)
972    }
973}
974
975impl<F: FileSystem> Seek for FsFile<F> {
976    fn seek(&mut self, position: IoSeekFrom) -> io::Result<u64> {
977        self.seek_native(position).map_err(io_error)
978    }
979}
980
981/// Typed read request for [`FsClient::readv`].
982pub struct FsRead<'a, F: FileSystem> {
983    file: &'a FsFile<F>,
984    offset: VfOffset,
985    length: usize,
986}
987
988/// Typed borrowed write request for [`FsClient::writev`].
989pub struct FsWrite<'a, F: FileSystem> {
990    file: &'a FsFile<F>,
991    offset: VfOffset,
992    data: &'a [u8],
993}
994
995/// Typed vector read into caller-provided storage.
996pub struct FsReadInto<'a, F: FileSystem> {
997    file: &'a FsFile<F>,
998    offset: VfOffset,
999    buffer: &'a mut [u8],
1000}
1001
1002impl<F: FileSystem> Drop for FsFile<F> {
1003    fn drop(&mut self) {
1004        if let Some(file) = self.file.take()
1005            && let Ok(mut filesystem) = self.inner.lock()
1006        {
1007            let _ = filesystem.close_one(&file);
1008        }
1009    }
1010}