dia_files/
root.rs

1/*
2==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--
3
4Dia-Files
5
6Copyright (C) 2019-2025  Anonymous
7
8There are several releases over multiple years,
9they are listed as ranges, such as: "2019-2025".
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 {
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/// # File discovery
46///
47/// ## Notes
48///
49/// - You can make this struct by [`find_files()`][::find_files()].
50/// - If sub directories are symlinks, they will be ignored.
51///
52/// [::find_files()]: fn.find_files.html
53#[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 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!(fs::canonicalize(&path)) {
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 && 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="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    /// # Makes new instance
148    #[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    /// # 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="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
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="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/// # 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="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/// # Checks if two paths are on a same Unix device
267///
268/// If either file does not exist, the function returns `Ok(false)`.
269#[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/// # Checks if two paths are on a same Unix device
276///
277/// If either file does not exist, the function returns `Ok(false)`.
278#[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/// # Writes data to target file via a temporary file
339///
340/// ## Steps
341///
342/// - Make a temporary file in the same directory as your target file, then write to it.
343/// - On success, rename that file to your target file.
344///
345/// ## Notes
346///
347/// - Currently, file permissions are only supported on Unix. If you don't provide permissions:
348///
349///     + If target file exists, its permissions will be used.
350///     + If target file does not exist, default permissions when creating new files will be used. Normally, this is system-wide setting.
351///
352/// - The function returns an error if your temporary file suffix is either empty or just contains white spaces.
353/// - If the temporary file exists, it will be overwritten.
354/// - On failure, the function tries to delete the temporary file, then returns the error.
355#[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/// # Writes data to target file via a temporary file
363///
364/// ## Steps
365///
366/// - Make a temporary file in the same directory as your target file, then write to it.
367/// - On success, rename that file to your target file.
368///
369/// ## Notes
370///
371/// - Currently, file permissions are only supported on Unix. If you don't provide permissions:
372///
373///     + If target file exists, its permissions will be used.
374///     + If target file does not exist, default permissions when creating new files will be used. Normally, this is system-wide setting.
375///
376/// - The function returns an error if your temporary file suffix is either empty or just contains white spaces.
377/// - If the temporary file exists, it will be overwritten.
378/// - On failure, the function tries to delete the temporary file, then returns the error.
379#[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}