anni_common/
fs.rs

1use crate::decode::raw_to_string;
2use log::debug;
3use path_absolutize::*;
4use std::ffi::OsString;
5pub use std::fs::*;
6use std::path::{Path, PathBuf};
7use std::{fs, io};
8
9pub struct PathWalker {
10    path: Vec<PathBuf>,
11    files: Vec<PathBuf>,
12    recursive: bool,
13    // whether to treat symlink file as regular file
14    allow_symlink_file: bool,
15    ignores: Vec<OsString>,
16}
17
18impl Iterator for PathWalker {
19    type Item = PathBuf;
20
21    fn next(&mut self) -> Option<Self::Item> {
22        if self.files.is_empty() {
23            if self.path.is_empty() || !self.recursive {
24                return None;
25            }
26            while self.files.is_empty() && !self.path.is_empty() {
27                self.extract_path();
28            }
29            if self.files.is_empty() {
30                return None;
31            }
32        }
33
34        Some(self.files.remove(0))
35    }
36}
37
38impl PathWalker {
39    fn extract_path(&mut self) {
40        if self.recursive && !self.path.is_empty() {
41            let path = self.path.get(0).unwrap();
42            let mut dir: Vec<_> = read_dir(path).unwrap().map(|r| r.unwrap()).collect();
43            dir.sort_by_key(|e| e.path());
44            for entry in dir.iter() {
45                let metadata = entry.metadata().unwrap();
46                if self.ignores.contains(&entry.file_name()) {
47                    continue;
48                }
49
50                if metadata.is_dir() {
51                    self.path.push(entry.path());
52                } else if metadata.is_file() {
53                    self.files.push(entry.path());
54                } else {
55                    // symlink
56                    if self.allow_symlink_file {
57                        // if it's a file, add it to files
58                        if fs::metadata(entry.path()).unwrap().is_file() {
59                            self.files.push(entry.path());
60                        }
61                    }
62                }
63            }
64            self.path.remove(0);
65        }
66    }
67
68    pub fn new<P: AsRef<Path>>(
69        p: P,
70        recursive: bool,
71        allow_symlink_file: bool,
72        ignores: Vec<String>,
73    ) -> Self {
74        let mut path = Vec::new();
75        let mut files = Vec::new();
76        if is_dir(&p).unwrap() {
77            path.push(p.as_ref().to_owned());
78        } else {
79            files.push(p.as_ref().to_owned());
80        }
81        let mut walker = PathWalker {
82            path,
83            files,
84            recursive: true,
85            allow_symlink_file,
86            ignores: ignores.into_iter().map(|s| s.into()).collect(),
87        };
88        walker.extract_path();
89        walker.recursive = recursive;
90        walker
91    }
92
93    pub fn with_extensions(extensions: Box<[&str]>) -> Box<dyn Fn(&PathBuf) -> bool + '_> {
94        Box::new(move |file: &PathBuf| match file.extension() {
95            None => false,
96            Some(ext) => extensions.contains(&ext.to_str().unwrap()),
97        })
98    }
99}
100
101fn fs_walk_path<P: AsRef<Path>>(
102    path: P,
103    recursive: bool,
104    callback: &impl Fn(&Path) -> bool,
105) -> io::Result<bool> {
106    let meta = metadata(&path)?;
107    if meta.is_dir() && recursive {
108        let mut dir: Vec<_> = read_dir(path)?.map(|r| r.unwrap().path()).collect();
109        dir.sort();
110        for entry in dir {
111            if !fs_walk_path(entry, recursive, callback)? {
112                return Ok(false);
113            }
114        }
115        Ok(true)
116    } else {
117        Ok(callback(path.as_ref()))
118    }
119}
120
121pub fn walk_path<P: AsRef<Path>>(
122    path: P,
123    recursive: bool,
124    callback: impl Fn(&Path) -> bool,
125) -> io::Result<()> {
126    let _ = fs_walk_path(path, recursive, &callback)?;
127    Ok(())
128}
129
130pub fn is_dir<P: AsRef<Path>>(path: P) -> io::Result<bool> {
131    let meta = metadata(path.as_ref())?;
132    Ok(meta.is_dir())
133}
134
135pub fn get_ext_files<P: AsRef<Path>, T: AsRef<str>>(
136    dir: P,
137    ext: T,
138    recursive: bool,
139) -> io::Result<Vec<PathBuf>> {
140    let mut result = Vec::new();
141    if is_dir(dir.as_ref())? {
142        for file in PathWalker::new(dir.as_ref(), recursive, true, Default::default()) {
143            let file_ext = file
144                .extension()
145                .unwrap_or_default()
146                .to_str()
147                .unwrap_or_default();
148            if file_ext == ext.as_ref() {
149                result.push(file);
150            }
151        }
152    }
153    Ok(result)
154}
155
156pub fn get_ext_file<P: AsRef<Path>, T: AsRef<str>>(
157    dir: P,
158    ext: T,
159    recursive: bool,
160) -> io::Result<Option<PathBuf>> {
161    if is_dir(dir.as_ref())? {
162        for file in PathWalker::new(dir.as_ref(), recursive, true, Default::default()) {
163            let file_ext = file
164                .extension()
165                .unwrap_or_default()
166                .to_str()
167                .unwrap_or_default();
168            if file_ext == ext.as_ref() {
169                return Ok(Some(file));
170            }
171        }
172    }
173    Ok(None)
174}
175
176pub fn get_subdirectories<P: AsRef<Path>>(dir: P) -> io::Result<Vec<PathBuf>> {
177    let mut ret = Vec::new();
178    let mut dir: Vec<_> = read_dir(dir.as_ref())?.map(|r| r.unwrap()).collect();
179    dir.sort_by_key(|e| e.path());
180    for dir in dir.iter() {
181        let dir_type = dir.file_type()?;
182        if dir_type.is_dir() {
183            ret.push(dir.path());
184        }
185    }
186    Ok(ret)
187}
188
189pub fn read_to_string<P: AsRef<Path>>(input: P) -> io::Result<String> {
190    log::trace!("Reading file to string: {:?}", input.as_ref());
191    let r = read(input)?;
192    Ok(raw_to_string(&r))
193}
194
195#[cfg(feature = "trash")]
196pub fn remove_file<P: AsRef<Path>>(input: P, trashcan: bool) -> io::Result<()> {
197    if trashcan {
198        trash::delete(input.as_ref()).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
199    } else {
200        fs::remove_file(input)
201    }
202}
203
204#[cfg(feature = "trash")]
205pub fn remove_dir_all<P: AsRef<Path>>(path: P, trashcan: bool) -> io::Result<()> {
206    if trashcan {
207        trash::delete(path).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
208    } else {
209        fs::remove_dir_all(path)
210    }
211}
212
213/// Create symbolic link at `to` pointing to `from`
214pub fn symlink_file<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {
215    let link = path_diff(from, to.as_ref().parent().unwrap())?;
216    #[cfg(unix)]
217    return std::os::unix::fs::symlink(link, to);
218    #[cfg(windows)]
219    return std::os::windows::fs::symlink_file(link, to);
220}
221
222/// Create symbolic link at `to` pointing to `from`
223pub fn symlink_dir<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<()> {
224    let link = path_diff(from, to.as_ref().parent().unwrap())?;
225    #[cfg(unix)]
226    return std::os::unix::fs::symlink(link, to);
227    #[cfg(windows)]
228    return std::os::windows::fs::symlink_dir(link, to);
229}
230
231pub fn path_diff<P: AsRef<Path>, Q: AsRef<Path>>(path: P, base: Q) -> io::Result<PathBuf> {
232    Ok(pathdiff::diff_paths(path.as_ref().absolutize()?, base.as_ref().absolutize()?).unwrap())
233}
234
235pub fn copy_dir<P1, P2>(from: P1, to: P2) -> io::Result<()>
236where
237    P1: AsRef<Path>,
238    P2: AsRef<Path>,
239{
240    create_dir(to.as_ref())?;
241
242    for entry in read_dir(from)? {
243        let entry = entry?;
244        let file_type = entry.file_type()?;
245        let target = to.as_ref().join(entry.file_name());
246        if file_type.is_file() {
247            copy(entry.path(), target)?;
248        } else if file_type.is_dir() {
249            copy_dir(entry.path(), target)?;
250        }
251    }
252
253    Ok(())
254}
255
256/// Move a directory from one location to another.
257///
258/// This method uses [rename] at first. If [rename] fails with [io::ErrorKind::CrossesDevices],
259/// it will fallback to copying the directory and then removing the source directory.
260pub fn move_dir<P1, P2>(from: P1, to: P2) -> io::Result<()>
261where
262    P1: AsRef<Path>,
263    P2: AsRef<Path>,
264{
265    // check whether [from] is directory
266    if !is_dir(from.as_ref())? {
267        return Err(io::Error::new(
268            io::ErrorKind::InvalidInput,
269            format!("{} is not a directory", from.as_ref().display()),
270        ));
271    }
272
273    match rename(from.as_ref(), to.as_ref()) {
274        Err(e) if is_cross_device_error(&e) => {
275            debug!("Failed to rename across filesystems. Copying instead.");
276
277            copy_dir(from.as_ref(), to.as_ref())?;
278            debug!("Copying done. Removing source directory.");
279
280            fs::remove_dir_all(from.as_ref())?;
281            debug!("Source directory removed.");
282        }
283        _ => {}
284    };
285
286    Ok(())
287}
288
289/// Checks raw os error code of `error`.
290///
291/// Returns true if the code is [`EXDEV`](https://github.com/rust-lang/rust/blob/master/library/std/src/sys/unix/mod.rs#L284) on unix
292/// or [`ERROR_NOT_SAME_DEVICE`](https://github.com/rust-lang/rust/blob/master/library/std/src/sys/windows/mod.rs#L114) on windows
293fn is_cross_device_error(error: &io::Error) -> bool {
294    let code = error.raw_os_error();
295    #[cfg(windows)]
296    {
297        code == Some(17)
298    }
299    #[cfg(unix)]
300    {
301        code == Some(18)
302    }
303    #[cfg(all(not(windows), not(unix)))]
304    {
305        // unsupported platform
306        false
307    }
308}