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}