1use crate::{
30 CODE_NAME,
31 FilePermissions,
32 Result,
33 filter::Filter,
34};
35
36#[cfg(not(feature="async-std"))]
37use std::{
38 fs::{self, ReadDir},
39 path::{Path, PathBuf},
40};
41
42#[cfg(feature="async-std")]
43use async_std::{
44 fs::{self, ReadDir},
45 path::{Path, PathBuf},
46 stream::StreamExt,
47};
48
49#[derive(Debug)]
58pub struct FileDiscovery<F> {
59 root: PathBuf,
60 filter: F,
61 recursive: bool,
62 current: ReadDir,
63 sub_dirs: Option<Vec<PathBuf>>,
64 max_depth: Option<usize>,
65}
66
67macro_rules! make_file_discovery { ($dir: ident, $recursive: ident, $filter: ident, $max_depth: ident) => {{
68 Ok(Self {
69 root: async_call!($dir.as_ref().canonicalize())?,
70 filter: $filter,
71 recursive: $recursive,
72 current: async_call!(fs::read_dir($dir))?,
73 sub_dirs: None,
74 max_depth: $max_depth,
75 })
76}}}
77
78macro_rules! next { ($self: ident) => {{
79 loop {
80 match async_call!($self.current.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 && async_call!(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!(path.canonicalize()) {
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 && async_call!(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="async-std"))]
142 #[doc(cfg(not(feature="async-std")))]
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="async-std")]
149 #[doc(cfg(feature="async-std"))]
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="async-std")]
157#[doc(cfg(feature="async-std"))]
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="async-std"))]
182#[doc(cfg(not(feature="async-std")))]
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="async-std"))]
231#[doc(cfg(not(feature="async-std")))]
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="async-std")]
242#[doc(cfg(feature="async-std"))]
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
247macro_rules! on_same_unix_device { ($first: ident, $second: ident) => {{
248 use std::os::unix::fs::MetadataExt;
249
250 let first = $first.as_ref();
251 let second = $second.as_ref();
252
253 if async_call!(first.exists()) == false || async_call!(second.exists()) == false {
254 Ok(false)
255 } else {
256 Ok(async_call!(first.metadata())?.dev() == async_call!(second.metadata())?.dev())
257 }
258}}}
259
260#[cfg(all(not(feature="async-std"), unix))]
264#[doc(cfg(all(not(feature="async-std"), unix)))]
265pub fn on_same_unix_device<P, Q>(first: P, second: Q) -> Result<bool> where P: AsRef<Path>, Q: AsRef<Path> {
266 on_same_unix_device!(first, second)
267}
268
269#[cfg(all(feature="async-std", unix))]
273#[doc(cfg(all(feature="async-std", unix)))]
274pub async fn on_same_unix_device<P, Q>(first: P, second: Q) -> Result<bool> where P: AsRef<Path>, Q: AsRef<Path> {
275 on_same_unix_device!(first, second)
276}
277
278#[cfg(all(not(feature="async-std"), unix))]
279#[doc(cfg(all(not(feature="async-std"), unix)))]
280#[test]
281fn test_on_same_unix_device() -> Result<()> {
282 assert!(on_same_unix_device(file!(), PathBuf::from(file!()).parent().unwrap().join("lib.rs"))?);
283 assert!(on_same_unix_device(file!(), std::env::temp_dir())? == false);
284
285 Ok(())
286}
287
288macro_rules! write_file { ($target: ident, $file_permissions: ident, $data: ident, $tmp_file_suffix: ident) => {{
289 let target = $target.as_ref();
290
291 let tmp_file_suffix = $tmp_file_suffix.as_ref().trim();
292 if tmp_file_suffix.is_empty() {
293 return Err(err!("Temporary file suffix is empty"));
294 }
295
296 let tmp_file = match (target.parent(), target.file_name().map(|n| n.to_str())) {
297 (Some(dir), Some(Some(file_name))) => dir.join(format!("{file_name}.{tmp_file_suffix}.{CODE_NAME}.tmp")),
298 _ => return Err(err!("Failed to get host directory and/or file name of {target:?}")),
299 };
300
301 macro_rules! job { () => {{
302 #[cfg(unix)]
303 let file_permissions = match $file_permissions {
304 Some(file_permissions) => Some(file_permissions),
305 None => if async_call!(target.exists()) {
306 Some(async_call!(target.metadata())?.permissions().try_into()?)
307 } else {
308 None
309 },
310 };
311
312 async_call!(fs::write(&tmp_file, $data))?;
313
314 #[cfg(unix)]
315 if let Some(file_permissions) = file_permissions {
316 async_call!(file_permissions.set(&tmp_file))?;
317 }
318
319 async_call!(fs::rename(&tmp_file, target))
320 }}}
321 match job!() {
322 Ok(()) => Ok(()),
323 Err(err) => {
324 if async_call!(tmp_file.exists()) {
325 async_call!(fs::remove_file(tmp_file))?;
326 }
327 Err(err)
328 },
329 }
330}}}
331
332#[cfg(not(feature="async-std"))]
350#[doc(cfg(not(feature="async-std")))]
351pub fn write_file<P, B, S>(target: P, file_permissions: Option<FilePermissions>, data: B, tmp_file_suffix: S) -> Result<()>
352where P: AsRef<Path>, B: AsRef<[u8]>, S: AsRef<str> {
353 write_file!(target, file_permissions, data, tmp_file_suffix)
354}
355
356#[cfg(feature="async-std")]
374#[doc(cfg(feature="async-std"))]
375pub async fn write_file<P, B, S>(target: P, file_permissions: Option<FilePermissions>, data: B, tmp_file_suffix: S) -> Result<()>
376where P: AsRef<Path>, B: AsRef<[u8]>, S: AsRef<str> {
377 write_file!(target, file_permissions, data, tmp_file_suffix)
378}