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        self.lock()?.ensure_empty_dir_impl(path.as_ref())
110    }
111
112    /// Empty a directory while keeping it, with explicit removal policy.
113    pub(crate) fn remove_dir_contents_impl(
114        &self,
115        path: impl AsRef<Path>,
116        options: RemoveOptions,
117    ) -> VfResult<()> {
118        let path = path.as_ref();
119        if !self
120            .removal_metadata(path, "remove_dir_contents_impl")?
121            .is_dir()
122        {
123            return Err(
124                VfError::client(0, crate::ERR_NOTDIR).with_context("remove_dir_contents", path)
125            );
126        }
127        self.lock()?
128            .remove_dir_contents_path_with_options_impl(path, options)
129            .map_err(|error| error.with_context("remove_dir_contents", path))
130    }
131
132    pub(crate) fn vopen_dirs_impl<P: AsRef<Path>>(&self, paths: &[P]) -> VfResult<Vec<FsDir<F>>> {
133        if paths.is_empty() {
134            return Ok(Vec::new());
135        }
136        let mut backend = self.lock()?;
137        let mut output = Vec::with_capacity(paths.len());
138        for (index, path) in paths.iter().enumerate() {
139            let path = path.as_ref();
140            let dir = backend.open_dir_impl(path).map_err(|error| {
141                crate::application::vector_index(error, index).with_context("vopen_dirs", path)
142            })?;
143            if !matches!(dir, VfDir::Descriptor { .. }) {
144                let _ = backend.close_dir_impl(&dir);
145                return Err(VfError::unsupported(index).with_context("vopen_dirs", path));
146            }
147            output.push(FsDir {
148                inner: Arc::clone(&self.inner),
149                dir: Some(dir),
150                path: path.to_path_buf(),
151            });
152        }
153        Ok(output)
154    }
155
156    /// Preflight the entire vector before acquiring the mutation lock.
157    pub(crate) fn vremove_dir_contents_impl(
158        &self,
159        dirs: &[&FsDir<F>],
160        options: RemoveOptions,
161    ) -> VfResult<()> {
162        for (index, dir) in dirs.iter().enumerate() {
163            if !Arc::ptr_eq(&self.inner, &dir.inner) || dir.is_closed() {
164                return Err(VfError::client(index, crate::ERR_EBADF)
165                    .with_context("vremove_dir_contents", &dir.path));
166            }
167        }
168        if dirs.is_empty() {
169            return Ok(());
170        }
171        let mut backend = self.lock()?;
172        let mut first_error = None;
173        for (index, dir) in dirs.iter().enumerate() {
174            if let Err(error) = backend.remove_dir_contents_handle_with_options_impl(
175                dir.dir.as_ref().expect("preflighted"),
176                options,
177            ) {
178                let error = crate::application::vector_index(error, index)
179                    .with_context("vremove_dir_contents", &dir.path);
180                if error.is_transport() || !options.continue_on_error() {
181                    return Err(error);
182                }
183                first_error.get_or_insert(error);
184            }
185        }
186        first_error.map_or(Ok(()), Err)
187    }
188}
189
190impl<F: VectorBackend> FsClient<F> {
191    /// Remove paths in request order, optionally recursing into directories.
192    /// A successful prefix may remain if a later path fails.
193    #[doc(hidden)]
194    pub fn vremove_native<P: AsRef<Path>>(&self, paths: &[P], recursive: bool) -> VfResult<()> {
195        self.vremove_impl(paths, recursive, RemoveOptions::default())
196    }
197
198    /// Remove paths with explicit error, batching, and retry policy.
199    #[doc(hidden)]
200    pub fn vremove_impl<P: AsRef<Path>>(
201        &self,
202        paths: &[P],
203        recursive: bool,
204        options: RemoveOptions,
205    ) -> VfResult<()> {
206        let paths: Vec<&Path> = paths.iter().map(AsRef::as_ref).collect();
207        self.lock()?
208            .remove_paths_with_options_impl(&paths, recursive, options)
209            .map_err(|error| {
210                error
211                    .index()
212                    .and_then(|index| paths.get(index))
213                    .map_or(error.clone(), |path| {
214                        error.with_context("vremove_native", path)
215                    })
216            })
217    }
218}