mofu/walk_dir.rs
1//! Utilities for directory traversal.
2
3use std::fs;
4use std::fs::{Metadata, ReadDir};
5use std::io::{Error, ErrorKind, Result};
6use std::num::NonZero;
7use std::path::{Path, PathBuf};
8
9/// Returns an iterator that recursively traverses the specified directory.
10///
11/// Note that the iterator will skip any entries that produce errors. To
12/// handle errors explicitly, see [`WalkDir`].
13///
14/// # Parameters
15///
16/// - `path`: The directory path to traverse.
17/// - `max_depth`: Maximum depth to traverse:
18/// - `0` means unlimited depth.
19/// - `1` means only traverse top-level entries.
20///
21/// # Errors
22///
23/// This function will return an error in the following situations, but is not
24/// limited to just these cases:
25///
26/// - The provided `path` doesn't exist.
27/// - The process lacks permissions to view the contents.
28/// - The `path` points at a non-directory file.
29pub fn walk_dir<P>(path: P, max_depth: usize) -> Result<impl Iterator<Item = DirEntry>>
30where
31 P: AsRef<Path>,
32{
33 let max_depth = NonZero::new(max_depth);
34 let iter = WalkDir::new(path)?
35 .max_depth(max_depth)
36 .filter_map(Result::ok);
37 Ok(iter)
38}
39
40/// An iterator that recursively traverses the specified directory.
41///
42/// # Examples
43///
44/// ```
45/// use mofu::walk_dir::WalkDir;
46/// use std::num::NonZero;
47///
48/// let max_depth = NonZero::new(3);
49/// let iter = WalkDir::new(".")
50/// .unwrap()
51/// .max_depth(max_depth)
52/// .filter_map(Result::ok);
53/// for entry in iter {
54/// println!("{}", entry.path().display());
55/// }
56/// ```
57#[derive(Debug)]
58pub struct WalkDir {
59 stack: Vec<StackItem>,
60 max_depth: Option<NonZero<usize>>,
61}
62
63impl WalkDir {
64 /// Creates a new [`WalkDir`].
65 ///
66 /// # Errors
67 ///
68 /// This function will return an error in the following situations, but is not
69 /// limited to just these cases:
70 ///
71 /// - The provided `path` doesn't exist.
72 /// - The process lacks permissions to view the contents.
73 /// - The `path` points at a non-directory file.
74 pub fn new<P>(path: P) -> Result<Self>
75 where
76 P: AsRef<Path>,
77 {
78 let depth = unsafe { NonZero::new_unchecked(1) };
79 let iter = fs::read_dir(path)?;
80 let item = StackItem { depth, iter };
81 let stack = vec![item];
82 let max_depth = None;
83 Ok(Self { stack, max_depth })
84 }
85
86 /// Sets the maximum depth for traversal.
87 pub fn max_depth(mut self, max_depth: Option<NonZero<usize>>) -> Self {
88 self.max_depth = max_depth;
89 self
90 }
91}
92
93impl Iterator for WalkDir {
94 type Item = Result<DirEntry>;
95
96 fn next(&mut self) -> Option<Self::Item> {
97 let (depth, entry) = loop {
98 let item = self.stack.last_mut()?;
99 match item.iter.next() {
100 None => self.stack.pop(),
101 Some(Err(error)) => return Some(Err(error)),
102 Some(Ok(entry)) => break (item.depth, entry),
103 };
104 };
105
106 let entry = match DirEntry::try_from(entry) {
107 Err(error) => return Some(Err(error)),
108 Ok(entry) => entry,
109 };
110
111 if entry.metadata.is_dir() && self.max_depth.is_none_or(|max_depth| depth < max_depth) {
112 match fs::read_dir(entry.path()) {
113 // Yes, this branch is still reachable.
114 Err(error) if error.kind() == ErrorKind::NotADirectory => (),
115 Err(error) => return Some(Err(error)),
116 Ok(iter) => {
117 // Will not overflow because `depth < max_depth`.
118 let depth = unsafe { NonZero::new_unchecked(depth.get() + 1) };
119 let item = StackItem { depth, iter };
120 self.stack.push(item);
121 }
122 }
123 }
124
125 Some(Ok(entry))
126 }
127}
128
129/// A directory entry returned by [`WalkDir`].
130///
131/// Each entry provides a path along with cached metadata to help avoid
132/// redundant system calls. However, due to possible concurrent file access,
133/// the cached metadata may degrade in validity over time.
134///
135/// Note that the metadata does not follow symlinks.
136#[derive(Debug)]
137pub struct DirEntry {
138 path: PathBuf,
139 metadata: Metadata,
140}
141
142impl DirEntry {
143 /// Returns the path.
144 #[inline]
145 pub fn path(&self) -> &Path {
146 &self.path
147 }
148
149 /// Returns the cached metadata.
150 ///
151 /// Due to possible concurrent file access, the cached metadata may degrade in
152 /// validity over time.
153 ///
154 /// Note that the metadata does not follow symbolic links.
155 #[inline]
156 pub fn metadata(&self) -> &Metadata {
157 &self.metadata
158 }
159}
160
161impl TryFrom<fs::DirEntry> for DirEntry {
162 type Error = Error;
163
164 #[inline]
165 fn try_from(value: fs::DirEntry) -> Result<Self> {
166 let path = value.path();
167 let metadata = value.metadata()?;
168 Ok(Self { path, metadata })
169 }
170}
171
172impl TryFrom<PathBuf> for DirEntry {
173 type Error = Error;
174
175 #[inline]
176 fn try_from(value: PathBuf) -> Result<Self> {
177 let path = value;
178 let metadata = path.symlink_metadata()?;
179 Ok(Self { path, metadata })
180 }
181}
182
183impl From<DirEntry> for PathBuf {
184 #[inline]
185 fn from(value: DirEntry) -> Self {
186 value.path
187 }
188}
189
190#[derive(Debug)]
191struct StackItem {
192 depth: NonZero<usize>,
193 iter: ReadDir,
194}