fs_more/directory/scan/mod.rs
1use std::{
2 cmp::Ordering,
3 fs::Metadata,
4 path::{Path, PathBuf},
5};
6
7
8use_enabled_fs_module!();
9
10use crate::error::{DirectoryEmptinessScanError, DirectoryScanError};
11
12pub(crate) mod collected;
13mod iter;
14pub use iter::*;
15
16
17
18
19/// The maximum directory scan depth option.
20///
21/// Used primarily in [`DirectoryScanner`].
22#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
23pub enum DirectoryScanDepthLimit {
24 /// No scan depth limit.
25 Unlimited,
26
27 /// Scan depth is limited to `maximum_depth`, where the value refers to
28 /// the maximum depth of the subdirectory whose contents are still listed.
29 ///
30 ///
31 /// # Examples
32 /// `maximum_depth = 0` indicates a scan that will cover only the files and directories
33 /// directly in the source directory.
34 ///
35 /// ```md
36 /// ~/scanned-directory
37 /// |- foo.csv
38 /// |- foo-2.csv
39 /// |- bar/
40 /// (no entries listed)
41 /// ```
42 ///
43 /// Notice how *contents* of the `~/scanned-directory/bar/`
44 /// directory are not returned in the scan when using depth `0`.
45 ///
46 ///
47 /// <br>
48 ///
49 /// `maximum_depth = 1` will cover the files and directories directly in the source directory
50 /// plus one level of files and subdirectories deeper.
51 ///
52 /// ```md
53 /// ~/scanned-directory
54 /// |- foo.csv
55 /// |- foo-2.csv
56 /// |- bar/
57 /// |- hello-world.txt
58 /// |- bar2/
59 /// (no entries listed)
60 /// ```
61 ///
62 /// Notice how contents of `~/scanned-directory/bar` are listed,
63 /// but contents of `~/scanned-directory/bar/bar2` are not.
64 Limited {
65 /// Maximum scan depth.
66 maximum_depth: usize,
67 },
68}
69
70
71/// Options that influence [`DirectoryScanner`].
72#[derive(Clone, PartialEq, Eq, Debug)]
73pub struct DirectoryScanOptions {
74 /// Whether to have the iterator yield the base directory
75 /// as its first item or not.
76 pub yield_base_directory: bool,
77
78 /// The maximum directory scanning depth, see [`DirectoryScanDepthLimit`].
79 pub maximum_scan_depth: DirectoryScanDepthLimit,
80
81 /// If enabled, symlinks inside the scan tree will be followed,
82 /// meaning yielded [`ScanEntry`] elements will have their paths
83 /// resolved in case of a symlink.
84 ///
85 /// If a symlink cycle is detected inside the tree,
86 /// an error is returned when it is encountered.
87 pub follow_symbolic_links: bool,
88
89 /// If enabled, and if the base directory is a symbolic link,
90 /// the iterator will first resolve the symbolic link,
91 /// then proceed with scanning the destination. If the symbolic link
92 /// does not point to a directory, an error will be returned from
93 /// the first call to iterator's [`next`].
94 ///
95 /// If disabled, and the base directory is a symbolic link,
96 /// the iterator will either yield only the base directory
97 /// (if `yield_base_directory` is true), or nothing.
98 ///
99 /// This has no effect if the base directory is not a symlink.
100 ///
101 ///
102 /// [`next`]: BreadthFirstDirectoryIter::next
103 pub follow_base_directory_symbolic_link: bool,
104}
105
106impl DirectoryScanOptions {
107 #[inline]
108 pub(crate) const fn should_track_ancestors(&self) -> bool {
109 self.follow_symbolic_links
110 }
111}
112
113impl Default for DirectoryScanOptions {
114 fn default() -> Self {
115 Self {
116 yield_base_directory: true,
117 maximum_scan_depth: DirectoryScanDepthLimit::Unlimited,
118 follow_symbolic_links: false,
119 follow_base_directory_symbolic_link: false,
120 }
121 }
122}
123
124
125
126/// Describes the depth of a scanned entry.
127///
128/// The depth is usually relative to a base directory (e.g. to the scan root).
129#[derive(Clone, Copy, PartialEq, Eq, Debug)]
130pub enum ScanEntryDepth {
131 /// The given entry is the base directory of the scan.
132 BaseDirectory,
133
134 /// The given entry is at `depth` levels under the base directory.
135 AtDepth {
136 /// Describes the depth of the scan entry.
137 ///
138 /// In this context, 0 means the entry is a direct descendant of the base directory,
139 /// 1 means it is a grandchild, and so on.
140 depth: usize,
141 },
142}
143
144impl ScanEntryDepth {
145 /// Returns a [`ScanEntryDepth`] that is one level deeper than the current one.
146 fn plus_one_level(self) -> Self {
147 match self {
148 ScanEntryDepth::BaseDirectory => ScanEntryDepth::AtDepth { depth: 0 },
149 ScanEntryDepth::AtDepth { depth } => ScanEntryDepth::AtDepth { depth: depth + 1 },
150 }
151 }
152}
153
154impl PartialOrd for ScanEntryDepth {
155 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
156 match (self, other) {
157 (ScanEntryDepth::BaseDirectory, ScanEntryDepth::BaseDirectory) => Some(Ordering::Equal),
158 (ScanEntryDepth::BaseDirectory, ScanEntryDepth::AtDepth { .. }) => Some(Ordering::Less),
159 (ScanEntryDepth::AtDepth { .. }, ScanEntryDepth::BaseDirectory) => {
160 Some(Ordering::Greater)
161 }
162 (
163 ScanEntryDepth::AtDepth { depth: left_depth },
164 ScanEntryDepth::AtDepth { depth: right_depth },
165 ) => left_depth.partial_cmp(right_depth),
166 }
167 }
168}
169
170
171/// A directory scan entry.
172pub struct ScanEntry {
173 path: PathBuf,
174
175 metadata: Metadata,
176
177 depth: ScanEntryDepth,
178}
179
180impl ScanEntry {
181 #[inline]
182 fn new(path: PathBuf, metadata: Metadata, depth: ScanEntryDepth) -> Self {
183 Self {
184 path,
185 metadata,
186 depth,
187 }
188 }
189
190 /// Returns the depth of the entry inside the scan tree.
191 pub fn depth(&self) -> &ScanEntryDepth {
192 &self.depth
193 }
194
195 /// Returns the [`Path`] of the scan entry.
196 pub fn path(&self) -> &Path {
197 &self.path
198 }
199
200 /// Returns the [`Metadata`] of the scan entry.
201 pub fn metadata(&self) -> &Metadata {
202 &self.metadata
203 }
204
205 /// Consumes `self` and returns the owned path ([`PathBuf`]) of the scan entry.
206 pub fn into_path(self) -> PathBuf {
207 self.path
208 }
209
210 /// Consumes `self` and returns the [`Metadata`] of the scan entry.
211 pub fn into_metadata(self) -> Metadata {
212 self.metadata
213 }
214
215 /// Consumes `self` and returns the path ([`PathBuf`])
216 /// and the [`Metadata`] of the scan entry.
217 pub fn into_path_and_metadata(self) -> (PathBuf, Metadata) {
218 (self.path, self.metadata)
219 }
220}
221
222
223
224
225/// A directory scanner with configurable iteration behaviour.
226///
227///
228/// # Alternatives
229///
230/// This scanner is able to recursively iterate over the directory
231/// as well as optionally follow symbolic links. If, however, you're
232/// looking for something with a bit more features, such as sorting,
233/// and a longer history of ecosystem use, consider the
234/// [`walkdir`](https://docs.rs/walkdir) crate,
235/// which this scanner has been inspired by.
236pub struct DirectoryScanner {
237 base_path: PathBuf,
238
239 options: DirectoryScanOptions,
240}
241
242impl DirectoryScanner {
243 /// Initializes the directory scanner.
244 ///
245 /// This call will not interact with the filesystem yet. To turn this scanner struct into
246 /// a breadth-first recursive iterator, call its [`into_iter`][`Self::into_iter`] method.
247 pub fn new<P>(base_directory_path: P, options: DirectoryScanOptions) -> Self
248 where
249 P: Into<PathBuf>,
250 {
251 Self {
252 base_path: base_directory_path.into(),
253 options,
254 }
255 }
256}
257
258impl IntoIterator for DirectoryScanner {
259 type IntoIter = BreadthFirstDirectoryIter;
260 type Item = Result<ScanEntry, DirectoryScanError>;
261
262 fn into_iter(self) -> Self::IntoIter {
263 BreadthFirstDirectoryIter::new(self.base_path, self.options)
264 }
265}
266
267
268
269/// Returns `Ok(true)` if the given directory is completely empty, `Ok(false)` is it is not,
270/// `Err(_)` if the read fails.
271///
272/// Does not check whether the path exists or whether it is actually a directory,
273/// meaning the error return type is a very uninformative [`std::io::Error`].
274///
275/// Intended for internal use.
276pub(crate) fn is_directory_empty_unchecked(directory_path: &Path) -> std::io::Result<bool> {
277 let mut directory_read = fs::read_dir(directory_path)?;
278
279 let Some(first_entry_result) = directory_read.next() else {
280 return Ok(true);
281 };
282
283 first_entry_result?;
284
285 Ok(false)
286}
287
288
289/// Returns a `bool` indicating whether the given directory is completely empty.
290///
291/// Permission and other errors will *not* be coerced into `false`,
292/// but will instead raise a distinct error (see [`DirectoryEmptinessScanError`]).
293pub fn is_directory_empty<P>(directory_path: P) -> Result<bool, DirectoryEmptinessScanError>
294where
295 P: AsRef<Path>,
296{
297 // TODO Needs a more comprehensive set of tests for edge cases (though I'm not sure what they are yet).
298
299 let directory_path: &Path = directory_path.as_ref();
300
301 let directory_metadata =
302 fs::metadata(directory_path).map_err(|_| DirectoryEmptinessScanError::NotFound {
303 path: directory_path.to_path_buf(),
304 })?;
305
306 if !directory_metadata.is_dir() {
307 return Err(DirectoryEmptinessScanError::NotADirectory {
308 path: directory_path.to_path_buf(),
309 });
310 }
311
312
313 let mut directory_read = fs::read_dir(directory_path).map_err(|error| {
314 DirectoryEmptinessScanError::UnableToReadDirectory {
315 directory_path: directory_path.to_path_buf(),
316 error,
317 }
318 })?;
319
320
321 let Some(first_entry_result) = directory_read.next() else {
322 return Ok(true);
323 };
324
325 if let Err(first_entry_error) = first_entry_result {
326 return Err(DirectoryEmptinessScanError::UnableToReadDirectoryEntry {
327 directory_path: directory_path.to_path_buf(),
328 error: first_entry_error,
329 });
330 }
331
332 Ok(false)
333}