dia_files/
root.rs

1/*
2==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--
3
4Dia-Files
5
6Copyright (C) 2019-2024  Anonymous
7
8There are several releases over multiple years,
9they are listed as ranges, such as: "2019-2024".
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU Lesser General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19GNU Lesser General Public License for more details.
20
21You should have received a copy of the GNU Lesser General Public License
22along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
24::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
25*/
26
27//! # Root
28
29use 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/// # File discovery
50///
51/// ## Notes
52///
53/// - You can make this struct by [`find_files()`][::find_files()].
54/// - If sub directories are symlinks, they will be ignored.
55///
56/// [::find_files()]: fn.find_files.html
57#[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 we accept symlinks, update code that calls ancestors()!
88                            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                                    // Symlinks are ignored, so using ancestors() is safe.
95                                    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    /// # Makes new instance
141    #[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    /// # Makes new instance
148    #[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    /// # Finds next path
161    pub async fn next(&mut self) -> Option<Result<PathBuf>> {
162        next!(self)
163    }
164
165    /// # Counts all files
166    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
193/// # Calculates depth of path from a root directory
194///
195/// ## Notes
196///
197/// - [`canonicalize()`][r://PathBuf/canonicalize()] is _not_ called on input paths.
198///
199/// [r://PathBuf/canonicalize()]: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.canonicalize
200fn 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/// # Finds files
226///
227/// This function makes new instance of [`FileDiscovery`][::FileDiscovery]. You should refer to that struct for notes on usage.
228///
229/// [::FileDiscovery]: struct.FileDiscovery.html
230#[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/// # Finds files
237///
238/// This function makes new instance of [`FileDiscovery`][::FileDiscovery]. You should refer to that struct for notes on usage.
239///
240/// [::FileDiscovery]: struct.FileDiscovery.html
241#[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/// # Checks if two paths are on a same Unix device
261///
262/// If either file does not exist, the function returns `Ok(false)`.
263#[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/// # Checks if two paths are on a same Unix device
270///
271/// If either file does not exist, the function returns `Ok(false)`.
272#[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/// # Writes data to target file via a temporary file
333///
334/// ## Steps
335///
336/// - Make a temporary file in the same directory as your target file, then write to it.
337/// - On success, rename that file to your target file.
338///
339/// ## Notes
340///
341/// - Currently, file permissions are only supported on Unix. If you don't provide permissions:
342///
343///     + If target file exists, its permissions will be used.
344///     + If target file does not exist, default permissions when creating new files will be used. Normally, this is system-wide setting.
345///
346/// - The function returns an error if your temporary file suffix is either empty or just contains white spaces.
347/// - If the temporary file exists, it will be overwritten.
348/// - On failure, the function tries to delete the temporary file, then returns the error.
349#[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/// # Writes data to target file via a temporary file
357///
358/// ## Steps
359///
360/// - Make a temporary file in the same directory as your target file, then write to it.
361/// - On success, rename that file to your target file.
362///
363/// ## Notes
364///
365/// - Currently, file permissions are only supported on Unix. If you don't provide permissions:
366///
367///     + If target file exists, its permissions will be used.
368///     + If target file does not exist, default permissions when creating new files will be used. Normally, this is system-wide setting.
369///
370/// - The function returns an error if your temporary file suffix is either empty or just contains white spaces.
371/// - If the temporary file exists, it will be overwritten.
372/// - On failure, the function tries to delete the temporary file, then returns the error.
373#[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}