Skip to main content

vfsi_sync/client/
directory.rs

1//! Directory traversal and retained-directory operations.
2
3use super::*;
4
5impl<F: VectorBackend> FsClient<F> {
6    fn removal_metadata(&self, path: &Path, operation: &'static str) -> VfResult<Attrs> {
7        let mut filesystem = self.lock()?;
8        let follow = !filesystem.capabilities().contains(Capabilities::LSTAT);
9        filesystem
10            .metadata_path_impl(path, follow)
11            .map_err(|error| error.with_context(operation, path))
12    }
13}
14
15impl<F: VectorBackend> FsClient<F> {
16    /// Lazy between directories, with selective no-follow metadata and pruning.
17    /// Enter runs before any listing; Leave follows even a pruned directory.
18    /// Sorting buffers one bounded directory, not the whole tree. The callback
19    /// runs outside the backend lock. Limits include the starting object.
20    /// Maximum safe cohort size for incremental directory paging.
21    #[doc(hidden)]
22    pub fn directory_page_batch_size(&self) -> VfResult<usize> {
23        Ok(self.lock()?.directory_page_batch_size().clamp(1, 32))
24    }
25
26    /// Fetch a bounded vector of directory pages, retaining backend cursors.
27    #[doc(hidden)]
28    pub fn read_dir_pages_with_fields(
29        &self,
30        paths: &[&Path],
31        fields: AttrMask,
32        cursors: Vec<Option<crate::DirPageCursor>>,
33        page_size: usize,
34        max_entries: usize,
35        follow_symlinks: bool,
36    ) -> VfResult<Vec<crate::DirectoryPage>> {
37        let pages = self.lock()?.vlistdir_pages_impl(
38            paths,
39            fields | AttrMask::MODE | AttrMask::SIZE,
40            cursors,
41            page_size,
42            max_entries,
43            follow_symlinks,
44        )?;
45        if pages.len() != paths.len() {
46            return Err(VfError::transport(
47                None,
48                "directory pages returned an invalid result count",
49            ));
50        }
51        pages
52            .into_iter()
53            .zip(paths)
54            .enumerate()
55            .map(|(index, ((attrs, next, children), path))| {
56                if children.len() != attrs.len()
57                    || attrs.len() > page_size
58                    || (attrs.is_empty() && next.is_some())
59                {
60                    return Err(VfError::transport(
61                        Some(index),
62                        "invalid directory page progress",
63                    ));
64                }
65                let mut seeds = Vec::new();
66                let entries = attrs
67                    .into_iter()
68                    .zip(children)
69                    .map(|(attrs, child)| {
70                        let entry_path = attrs
71                            .file
72                            .path()
73                            .ok_or_else(|| {
74                                VfError::transport(Some(index), "directory entry has no path")
75                            })?
76                            .to_path_buf();
77                        if entry_path.parent() != Some(*path) {
78                            return Err(VfError::transport(
79                                Some(index),
80                                "directory entry is outside its parent",
81                            ));
82                        }
83                        if let Some(child) = child {
84                            seeds.push((entry_path.clone(), child));
85                        }
86                        Ok(DirEntry::new(
87                            entry_path,
88                            vfsi_core::metadata_from_attrs(attrs),
89                        ))
90                    })
91                    .collect::<VfResult<Vec<_>>>()?;
92                Ok((
93                    DirectoryListing {
94                        path: path.to_path_buf(),
95                        entries,
96                    },
97                    next,
98                    seeds,
99                ))
100            })
101            .collect()
102    }
103}
104
105impl<F: VectorBackend> FsClient<F> {
106    /// Create `path` if missing, otherwise empty it. Errors if it exists and is
107    /// not a directory (a symlink to a directory is not a directory here).
108    pub fn ensure_empty_dir(&self, path: impl AsRef<Path>) -> VfResult<()> {
109        let path = path.as_ref();
110        self.lock()?.ensure_empty_dir_impl(path)
111    }
112
113    /// Empty a directory while keeping it, with explicit removal policy.
114    pub(crate) fn remove_dir_contents_impl(
115        &self,
116        path: impl AsRef<Path>,
117        options: RemoveOptions,
118    ) -> VfResult<()> {
119        let path = path.as_ref();
120        if !self
121            .removal_metadata(path, "remove_dir_contents_impl")?
122            .is_dir()
123        {
124            return Err(
125                VfError::client(0, crate::ERR_NOTDIR).with_context("remove_dir_contents", path)
126            );
127        }
128        self.lock()?
129            .remove_dir_contents_path_with_options_impl(path, options)
130            .map_err(|error| error.with_context("remove_dir_contents", path))
131    }
132
133    pub(crate) fn vopen_dirs_impl<P: AsRef<Path>>(&self, paths: &[P]) -> VfResult<Vec<FsDir<F>>> {
134        if paths.is_empty() {
135            return Ok(Vec::new());
136        }
137        // Convert caller paths and allocate diagnostic storage before locking.
138        // These are the same handle/path allocations required on success.
139        let mut output: Vec<_> = paths
140            .iter()
141            .map(|path| FsDir {
142                inner: Arc::clone(&self.inner),
143                dir: None,
144                path: path.as_ref().to_path_buf(),
145            })
146            .collect();
147        let mut backend = self.lock()?;
148        for (index, handle) in output.iter_mut().enumerate() {
149            let dir = backend.open_dir_impl(&handle.path).map_err(|error| {
150                crate::application::vector_index(error, index)
151                    .with_context("vopen_dirs", &handle.path)
152            })?;
153            if !matches!(dir, VfDir::Descriptor { .. }) {
154                let _ = backend.close_dir_impl(&dir);
155                return Err(VfError::unsupported(index).with_context("vopen_dirs", &handle.path));
156            }
157            handle.dir = Some(dir);
158        }
159        Ok(output)
160    }
161
162    /// Preflight the entire vector before acquiring the mutation lock.
163    pub(crate) fn vremove_dir_contents_impl(
164        &self,
165        dirs: &[&FsDir<F>],
166        options: RemoveOptions,
167    ) -> VfResult<()> {
168        for (index, dir) in dirs.iter().enumerate() {
169            if !Arc::ptr_eq(&self.inner, &dir.inner) || dir.is_closed() {
170                return Err(VfError::client(index, crate::ERR_EBADF)
171                    .with_context("vremove_dir_contents", &dir.path));
172            }
173        }
174        if dirs.is_empty() {
175            return Ok(());
176        }
177        let mut backend = self.lock()?;
178        let mut first_error = None;
179        for (index, dir) in dirs.iter().enumerate() {
180            if let Err(error) = backend.remove_dir_contents_handle_with_options_impl(
181                dir.dir.as_ref().expect("preflighted"),
182                options,
183            ) {
184                let error = crate::application::vector_index(error, index)
185                    .with_context("vremove_dir_contents", &dir.path);
186                if error.is_transport() || !options.continue_on_error() {
187                    return Err(error);
188                }
189                first_error.get_or_insert(error);
190            }
191        }
192        first_error.map_or(Ok(()), Err)
193    }
194}
195
196impl<F: VectorBackend> FsClient<F> {
197    /// Remove paths in request order, optionally recursing into directories.
198    /// A successful prefix may remain if a later path fails.
199    #[doc(hidden)]
200    pub fn vremove_native<P: AsRef<Path>>(&self, paths: &[P], recursive: bool) -> VfResult<()> {
201        self.vremove_impl(paths, recursive, RemoveOptions::default())
202    }
203
204    /// Remove paths with explicit error, batching, and retry policy.
205    #[doc(hidden)]
206    pub fn vremove_impl<P: AsRef<Path>>(
207        &self,
208        paths: &[P],
209        recursive: bool,
210        options: RemoveOptions,
211    ) -> VfResult<()> {
212        let paths: Vec<&Path> = paths.iter().map(AsRef::as_ref).collect();
213        self.lock()?
214            .remove_paths_with_options_impl(&paths, recursive, options)
215            .map_err(|error| {
216                with_indexed_context(error, "vremove_native", |index| paths.get(index).copied())
217            })
218    }
219}