1use {
30 std::path::{Path, PathBuf},
31 crate::{
32 CODE_NAME,
33 FilePermissions,
34 Result,
35 filter::Filter,
36 },
37};
38
39#[cfg(not(feature="tokio"))]
40use std::fs::{self, ReadDir};
41
42#[cfg(feature="tokio")]
43use tokio::fs::{self, ReadDir};
44
45#[derive(Debug)]
54pub struct FileDiscovery<F> {
55 root: PathBuf,
56 filter: F,
57 recursive: bool,
58 current: ReadDir,
59 sub_dirs: Option<Vec<PathBuf>>,
60 max_depth: Option<usize>,
61}
62
63macro_rules! make_file_discovery { ($dir: ident, $recursive: ident, $filter: ident, $max_depth: ident) => {{
64 Ok(Self {
65 root: async_call!(fs::canonicalize($dir.as_ref()))?,
66 filter: $filter,
67 recursive: $recursive,
68 current: async_call!(fs::read_dir($dir))?,
69 sub_dirs: None,
70 max_depth: $max_depth,
71 })
72}}}
73
74macro_rules! next { ($self: ident) => {{
75 loop {
76 #[cfg(not(feature="tokio"))]
77 let next = $self.current.next();
78 #[cfg(feature="tokio")]
79 let next = $self.current.next_entry().await.transpose();
80 match next {
81 Some(Ok(entry)) => {
82 match async_call!(entry.file_type()) {
83 Ok(file_type) => {
84 let path = entry.path();
85 let is_symlink = file_type.is_symlink();
86 if file_type.is_dir() || (is_symlink && path.is_dir()) {
87 if $self.recursive == false || is_symlink {
89 continue;
90 }
91
92 if let Some(max_depth) = $self.max_depth.as_ref() {
93 match async_call!(fs::canonicalize(&path)) {
94 Ok(path) => match depth_from(&$self.root, &path) {
96 Ok(depth) => if &depth >= max_depth {
97 continue;
98 },
99 Err(err) => return Some(Err(err)),
100 },
101 Err(err) => return Some(Err(err)),
102 };
103 }
104
105 if async_call!($self.filter.accept(&path)) == false {
106 continue;
107 }
108
109 match $self.sub_dirs.as_mut() {
110 Some(sub_dirs) => sub_dirs.push(path),
111 None => $self.sub_dirs = Some(vec!(path)),
112 };
113 } else if file_type.is_file() || (is_symlink && path.is_file()) {
114 if async_call!($self.filter.accept(&path)) == false {
115 continue;
116 }
117 return Some(Ok(path));
118 }
119 },
120 Err(err) => return Some(Err(err)),
121 };
122 },
123 Some(Err(err)) => return Some(Err(err)),
124 None => match $self.sub_dirs.as_mut() {
125 None => return None,
126 Some(sub_dirs) => match sub_dirs.len() {
127 0 => return None,
128 _ => match async_call!(fs::read_dir(sub_dirs.remove(0))) {
129 Ok(new) => $self.current = new,
130 Err(err) => return Some(Err(err)),
131 },
132 },
133 },
134 };
135 }
136}}}
137
138impl<'a> FileDiscovery<Filter<'a>> {
139
140 #[cfg(not(feature="tokio"))]
142 #[doc(cfg(not(feature="tokio")))]
143 pub fn make<P>(dir: P, recursive: bool, filter: Filter<'a>, max_depth: Option<usize>) -> Result<Self> where P: AsRef<Path> {
144 make_file_discovery!(dir, recursive, filter, max_depth)
145 }
146
147 #[cfg(feature="tokio")]
149 #[doc(cfg(feature="tokio"))]
150 pub async fn make<P>(dir: P, recursive: bool, filter: Filter<'a>, max_depth: Option<usize>) -> Result<Self> where P: AsRef<Path> {
151 make_file_discovery!(dir, recursive, filter, max_depth)
152 }
153
154}
155
156#[cfg(feature="tokio")]
157#[doc(cfg(feature="tokio"))]
158impl FileDiscovery<Filter<'_>> {
159
160 pub async fn next(&mut self) -> Option<Result<PathBuf>> {
162 next!(self)
163 }
164
165 pub async fn count(mut self) -> Option<usize> {
167 let mut result = usize::MIN;
168 while let Some(path) = self.next().await {
169 if path.is_ok() {
170 result = match result.checked_add(1) {
171 None => return None,
172 Some(n) => n,
173 };
174 }
175 }
176 Some(result)
177 }
178
179}
180
181#[cfg(not(feature="tokio"))]
182#[doc(cfg(not(feature="tokio")))]
183impl Iterator for FileDiscovery<Filter<'_>> {
184
185 type Item = Result<PathBuf>;
186
187 fn next(&mut self) -> Option<Self::Item> {
188 next!(self)
189 }
190
191}
192
193fn depth_from<P, Q>(root_dir: P, path: Q) -> Result<usize> where P: AsRef<Path>, Q: AsRef<Path> {
201 let root_dir = root_dir.as_ref();
202 let path = path.as_ref();
203
204 let mut depth: usize = 0;
205 let mut found_root = false;
206 for a in path.ancestors().skip(1) {
207 if a == root_dir {
208 found_root = true;
209 break;
210 } else {
211 match depth.checked_add(1) {
212 Some(new_depth) => depth = new_depth,
213 None => return Err(err!("Directory level of {path:?} is too deep: {depth}")),
214 };
215 }
216 }
217
218 if found_root {
219 Ok(depth)
220 } else {
221 Err(err!("{path:?} was expected to be inside of {root_dir:?}, but not"))
222 }
223}
224
225#[cfg(not(feature="tokio"))]
231#[doc(cfg(not(feature="tokio")))]
232pub fn find_files<'a, P>(dir: P, recursive: bool, filter: Filter<'a>) -> Result<FileDiscovery<Filter<'a>>> where P: AsRef<Path> {
233 FileDiscovery::make(dir, recursive, filter, None)
234}
235
236#[cfg(feature="tokio")]
242#[doc(cfg(feature="tokio"))]
243pub async fn find_files<'a, P>(dir: P, recursive: bool, filter: Filter<'a>) -> Result<FileDiscovery<Filter<'a>>> where P: AsRef<Path> {
244 FileDiscovery::make(dir, recursive, filter, None).await
245}
246
247#[cfg(not(feature="tokio"))]
248macro_rules! exists { ($p: expr) => { $p.exists() }}
249
250#[cfg(feature="tokio")]
251macro_rules! exists { ($p: expr) => { fs::try_exists($p).await? }}
252
253macro_rules! on_same_unix_device { ($first: ident, $second: ident) => {{
254 use std::os::unix::fs::MetadataExt;
255
256 let first = $first.as_ref();
257 let second = $second.as_ref();
258
259 if exists!(first) == false || exists!(second) == false {
260 Ok(false)
261 } else {
262 Ok(async_call!(fs::metadata(first))?.dev() == async_call!(fs::metadata(second))?.dev())
263 }
264}}}
265
266#[cfg(all(not(feature="tokio"), unix))]
270#[doc(cfg(all(not(feature="tokio"), unix)))]
271pub fn on_same_unix_device<P, Q>(first: P, second: Q) -> Result<bool> where P: AsRef<Path>, Q: AsRef<Path> {
272 on_same_unix_device!(first, second)
273}
274
275#[cfg(all(feature="tokio", unix))]
279#[doc(cfg(all(feature="tokio", unix)))]
280pub async fn on_same_unix_device<P, Q>(first: P, second: Q) -> Result<bool> where P: AsRef<Path>, Q: AsRef<Path> {
281 on_same_unix_device!(first, second)
282}
283
284#[cfg(all(not(feature="tokio"), unix))]
285#[doc(cfg(all(not(feature="tokio"), unix)))]
286#[test]
287fn test_on_same_unix_device() -> Result<()> {
288 assert!(on_same_unix_device(file!(), PathBuf::from(file!()).parent().unwrap().join("lib.rs"))?);
289 assert!(on_same_unix_device(file!(), std::env::temp_dir())? == false);
290
291 Ok(())
292}
293
294macro_rules! write_file { ($target: ident, $file_permissions: ident, $data: ident, $tmp_file_suffix: ident) => {{
295 let target = $target.as_ref();
296
297 let tmp_file_suffix = $tmp_file_suffix.as_ref().trim();
298 if tmp_file_suffix.is_empty() {
299 return Err(err!("Temporary file suffix is empty"));
300 }
301
302 let tmp_file = match (target.parent(), target.file_name().map(|n| n.to_str())) {
303 (Some(dir), Some(Some(file_name))) => dir.join(format!("{file_name}.{tmp_file_suffix}.{CODE_NAME}.tmp")),
304 _ => return Err(err!("Failed to get host directory and/or file name of {target:?}")),
305 };
306
307 macro_rules! job { () => {{
308 #[cfg(unix)]
309 let file_permissions = match $file_permissions {
310 Some(file_permissions) => Some(file_permissions),
311 None => if exists!(target) {
312 Some(async_call!(fs::metadata(target))?.permissions().try_into()?)
313 } else {
314 None
315 },
316 };
317
318 async_call!(fs::write(&tmp_file, $data))?;
319
320 #[cfg(unix)]
321 if let Some(file_permissions) = file_permissions {
322 async_call!(file_permissions.set(&tmp_file))?;
323 }
324
325 async_call!(fs::rename(&tmp_file, target))
326 }}}
327 match job!() {
328 Ok(()) => Ok(()),
329 Err(err) => {
330 if exists!(&tmp_file) {
331 async_call!(fs::remove_file(tmp_file))?;
332 }
333 Err(err)
334 },
335 }
336}}}
337
338#[cfg(not(feature="tokio"))]
356#[doc(cfg(not(feature="tokio")))]
357pub fn write_file<P, B, S>(target: P, file_permissions: Option<FilePermissions>, data: B, tmp_file_suffix: S) -> Result<()>
358where P: AsRef<Path>, B: AsRef<[u8]>, S: AsRef<str> {
359 write_file!(target, file_permissions, data, tmp_file_suffix)
360}
361
362#[cfg(feature="tokio")]
380#[doc(cfg(feature="tokio"))]
381pub async fn write_file<P, B, S>(target: P, file_permissions: Option<FilePermissions>, data: B, tmp_file_suffix: S) -> Result<()>
382where P: AsRef<Path>, B: AsRef<[u8]>, S: AsRef<str> {
383 write_file!(target, file_permissions, data, tmp_file_suffix)
384}