Skip to main content

unlab_gpu/
fs.rs

1//
2// Copyright (c) 2025-2026 Ɓukasz Szpakowski
3//
4// This Source Code Form is subject to the terms of the Mozilla Public
5// License, v. 2.0. If a copy of the MPL was not distributed with this
6// file, You can obtain one at https://mozilla.org/MPL/2.0/.
7//
8//! A filesytem module.
9use std::collections::HashSet;
10use std::fs::copy;
11use std::fs::create_dir;
12use std::fs::create_dir_all;
13use std::fs::read_dir;
14use std::fs::remove_dir;
15use std::fs::remove_file;
16use std::fs::symlink_metadata;
17use std::io::Error;
18use std::io::ErrorKind;
19use std::io::Result;
20use std::path::Path;
21use std::path::PathBuf;
22
23fn recursively_copy_with_path_bufs(src_path_buf: &mut PathBuf, dst_path_buf: &mut PathBuf) -> Result<()>
24{
25    let metadata = symlink_metadata(src_path_buf.as_path())?;
26    if metadata.is_dir() {
27        match create_dir(dst_path_buf.as_path()) {
28            Ok(()) => (),
29            Err(err) if err.kind() == ErrorKind::AlreadyExists => (),
30            Err(err) => return Err(err),
31        }
32        let canon_src_path_buf = src_path_buf.canonicalize()?;
33        let canon_dst_path_buf = dst_path_buf.canonicalize()?;
34        if canon_src_path_buf == canon_dst_path_buf {
35            return Ok(());
36        }
37        if canon_dst_path_buf.starts_with(canon_src_path_buf) {
38            remove_dir(dst_path_buf.as_path())?;
39            return Err(Error::new(ErrorKind::Other, "destination directory can't be in source directory"));
40        }
41        let entries = read_dir(src_path_buf.as_path())?;
42        for entry in entries {
43            let tmp_entry = entry?;
44            src_path_buf.push(tmp_entry.file_name());
45            dst_path_buf.push(tmp_entry.file_name());
46            recursively_copy_with_path_bufs(src_path_buf, dst_path_buf)?;
47            dst_path_buf.pop();
48            src_path_buf.pop();
49        }
50    } else if metadata.is_file() {
51        let canon_src_path_buf = src_path_buf.canonicalize()?;
52        match dst_path_buf.canonicalize() {
53            Ok(canon_dst_path_buf) => {
54                if canon_src_path_buf == canon_dst_path_buf {
55                    return Ok(());
56                }
57            },
58            Err(err) if err.kind() == ErrorKind::NotFound => (),
59            Err(err) => return Err(err),
60        }
61        copy(src_path_buf.as_path(), dst_path_buf.as_path())?;
62    } else if metadata.is_symlink() {
63        return Err(Error::new(ErrorKind::Other, "can't copy symbolic link"));
64    } else {
65        return Err(Error::new(ErrorKind::Other, "can't copy device file"));
66    }
67    Ok(())
68}
69
70/// Recursively copies the files and/or the directories.
71///
72/// This function automatically creates a destition directory with parent directories. If the
73/// canonical path of destination contains the canonical path of source, this function also
74/// returns an error. The file or the directory isn't copied if the canonical path of source is
75/// equal to the canonical path of destination. Symbolic links and device files aren't copied. If
76/// the symbolic link or the device file is occurred, this function also returns an error.
77pub fn recursively_copy<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()>
78{
79    let mut src_path_buf = PathBuf::from(src.as_ref());
80    let mut dst_path_buf = PathBuf::from(dst.as_ref());
81    let mut dst_parent_path_buf = dst_path_buf.clone();
82    dst_parent_path_buf.pop();
83    if dst_parent_path_buf != PathBuf::from("") {
84        create_dir_all(dst_parent_path_buf)?;
85    }
86    recursively_copy_with_path_bufs(&mut src_path_buf, &mut dst_path_buf)
87}
88
89fn recursively_remove_with_path_buf(path_buf: &mut PathBuf, is_force: bool) -> Result<()>
90{
91    let metadata = match symlink_metadata(path_buf.as_path()) {
92        Ok(tmp_metadata) => tmp_metadata,
93        Err(err) if is_force && err.kind() == ErrorKind::NotFound => return Ok(()),
94        Err(err) => return Err(err),
95    };
96    if metadata.is_dir() {
97        let entries = match read_dir(path_buf.as_path()) {
98            Ok(tmp_entries) => tmp_entries,
99            Err(err) if is_force && err.kind() == ErrorKind::NotFound => return Ok(()),
100            Err(err) => return Err(err),
101        };
102        for entry in entries {
103            let tmp_entry = entry?;
104            path_buf.push(tmp_entry.file_name());
105            recursively_remove_with_path_buf(path_buf, is_force)?;
106            path_buf.pop();
107        }
108        match remove_dir(path_buf.as_path()) {
109            Ok(()) => (),
110            Err(err) if is_force && err.kind() == ErrorKind::NotFound => (),
111            Err(err) => return Err(err),
112        }
113    } else {
114        match remove_file(path_buf.as_path()) {
115            Ok(()) => (),
116            Err(err) if is_force && err.kind() == ErrorKind::NotFound => (),
117            Err(err) => return Err(err),
118        }
119    }
120    Ok(())
121}
122
123/// Recursively removes the files and/or the directories.
124///
125/// This function doesn't returns an error for a directory or a file that doesn't exist if the
126/// force flag is set.
127pub fn recursively_remove<P: AsRef<Path>>(path: P, is_force: bool) -> Result<()>
128{
129    let mut path_buf = PathBuf::from(path.as_ref());
130    recursively_remove_with_path_buf(&mut path_buf, is_force)
131}
132
133fn get_dir_paths_and_paths_in_dir(path: &Path, suffix_path_buf: &mut PathBuf, dir_paths: &mut HashSet<PathBuf>, paths: &mut HashSet<PathBuf>, path_vec: &mut Option<Vec<PathBuf>>, ignored_paths: &HashSet<PathBuf>, depth: Option<usize>) -> Result<()>
134{
135    let mut path_buf = PathBuf::from(path);
136    if suffix_path_buf != &PathBuf::from("") {
137        path_buf.push(suffix_path_buf.as_path());
138    }
139    let metadata = match symlink_metadata(path_buf.as_path()) {
140        Ok(tmp_metadata) => tmp_metadata,
141        Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
142        Err(err) => return Err(err),
143    };
144    if metadata.is_dir() && depth.map(|d| d > 0).unwrap_or(true) {
145        match read_dir(path_buf.as_path()) {
146            Ok(entries) => {
147                dir_paths.insert(suffix_path_buf.clone());
148                for entry in entries {
149                    let tmp_entry = entry?;
150                    suffix_path_buf.push(tmp_entry.file_name());
151                    get_dir_paths_and_paths_in_dir(path, suffix_path_buf, dir_paths, paths, path_vec, ignored_paths, depth.map(|d| d - 1))?;
152                    suffix_path_buf.pop();
153                }
154            },
155            Err(err) if err.kind() == ErrorKind::NotFound => (),
156            Err(err) => return Err(err),
157        }
158    } else {
159        if !ignored_paths.contains(suffix_path_buf) {
160            paths.insert(suffix_path_buf.clone());
161            match path_vec {
162                Some(path_vec) => path_vec.push(suffix_path_buf.clone()),
163                None => (),
164            }
165        }
166    }
167    Ok(())
168}
169
170/// Searches the path conflicts for the source directory and the destination directory.
171///
172/// This function returns the conflict paths and the paths of source directory for the optional
173/// depth. The path of source directory are compared with the path of destination directory. If
174/// these paths are same, there occurs the path conflict.
175pub fn conflicts<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q, ignored_paths: &HashSet<PathBuf>, depth: Option<usize>) -> Result<(Vec<PathBuf>, Vec<PathBuf>)>
176{
177    let mut paths: Option<Vec<PathBuf>> = Some(Vec::new());
178    let mut src_dir_paths: HashSet<PathBuf> = HashSet::new();
179    let mut src_paths: HashSet<PathBuf> = HashSet::new();
180    let mut src_suffix_path_buf = PathBuf::from("");
181    get_dir_paths_and_paths_in_dir(src.as_ref(), &mut src_suffix_path_buf, &mut src_dir_paths, &mut src_paths, &mut paths, &HashSet::new(), depth)?;
182    let mut dst_dir_paths: HashSet<PathBuf> = HashSet::new();
183    let mut dst_paths: HashSet<PathBuf> = HashSet::new();
184    let mut dst_suffix_path_buf = PathBuf::from("");
185    get_dir_paths_and_paths_in_dir(dst.as_ref(), &mut dst_suffix_path_buf, &mut dst_dir_paths, &mut dst_paths, &mut None, ignored_paths, depth)?;
186    let mut conflict_paths: Vec<PathBuf> = src_dir_paths.intersection(&dst_paths).map(|p| p.clone()).collect();
187    let mut conflict_paths2: Vec<PathBuf> = src_paths.intersection(&dst_dir_paths).map(|p| p.clone()).collect();
188    let mut conflict_paths3: Vec<PathBuf> = src_paths.intersection(&dst_paths).map(|p| p.clone()).collect();
189    conflict_paths.append(&mut conflict_paths2);
190    conflict_paths.append(&mut conflict_paths3);
191    Ok((conflict_paths, paths.unwrap_or(Vec::new())))
192}
193
194fn get_paths_in_dir(path: &Path, suffix_path_buf: &mut PathBuf, paths: &mut Vec<PathBuf>, depth: Option<usize>) -> Result<()>
195{
196    let mut path_buf = PathBuf::from(path);
197    if suffix_path_buf != &PathBuf::from("") {
198        path_buf.push(suffix_path_buf.as_path());
199    }
200    let metadata = match symlink_metadata(path_buf.as_path()) {
201        Ok(tmp_metadata) => tmp_metadata,
202        Err(err) if err.kind() == ErrorKind::NotFound => return Ok(()),
203        Err(err) => return Err(err),
204    };
205    if metadata.is_dir() && depth.map(|d| d > 0).unwrap_or(true) {
206        match read_dir(path_buf.as_path()) {
207            Ok(entries) => {
208                for entry in entries {
209                    let tmp_entry = entry?;
210                    suffix_path_buf.push(tmp_entry.file_name());
211                    get_paths_in_dir(path, suffix_path_buf, paths, depth.map(|d| d - 1))?;
212                    suffix_path_buf.pop();
213                }
214            },
215            Err(err) if err.kind() == ErrorKind::NotFound => (),
216            Err(err) => return Err(err),
217        }
218    } else {
219        paths.push(suffix_path_buf.clone());
220    }
221    Ok(())
222}
223
224/// Returns the paths in the directory for the optional depth.
225pub fn paths_in_dir<P: AsRef<Path>>(path: P, depth: Option<usize>) -> Result<Vec<PathBuf>>
226{
227    let mut paths: Vec<PathBuf> = Vec::new();
228    let mut suffix_path_buf = PathBuf::from("");
229    get_paths_in_dir(path.as_ref(), &mut suffix_path_buf, &mut paths, depth)?;
230    Ok(paths)
231}
232
233/// Recursively copies the files and/or the directories which are in the directories of paths.
234///
235/// See [`recursively_copy`].
236pub fn recursively_copy_paths_in_dir<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q, paths: &[PathBuf]) -> Result<()>
237{
238    for suffix_path_buf in paths {
239        let mut src_path_buf = PathBuf::from(src.as_ref());
240        src_path_buf.push(suffix_path_buf.as_path());
241        let mut dst_path_buf = PathBuf::from(dst.as_ref());
242        dst_path_buf.push(suffix_path_buf.as_path());
243        recursively_copy(src_path_buf.as_path(), dst_path_buf.as_path())?;
244    }
245    Ok(())
246}
247
248/// Recursively removes the files and/or the directories which are in the directories of paths.
249///
250/// See [`recursively_remove`].
251pub fn recursively_remove_paths_in_dir<P: AsRef<Path>>(path: P, paths: &[PathBuf], is_force: bool) -> Result<()>
252{
253    for suffix_path_buf in paths {
254        let mut path_buf = PathBuf::from(path.as_ref());
255        path_buf.push(suffix_path_buf.as_path());
256        recursively_remove(path_buf.as_path(), is_force)?;
257        let mut tmp_suffix_path_buf = suffix_path_buf.clone();
258        if tmp_suffix_path_buf != PathBuf::from("") {
259            tmp_suffix_path_buf.pop();
260            while tmp_suffix_path_buf != PathBuf::from("") {
261                let mut dir_path_buf = PathBuf::from(path.as_ref());
262                dir_path_buf.push(tmp_suffix_path_buf.as_path());
263                match remove_dir(dir_path_buf.as_path()) {
264                    Ok(()) => (),
265                    Err(_) => break,
266                }
267                tmp_suffix_path_buf.pop();
268            }
269        }
270    }
271    Ok(())
272}
273
274/// Returns the path to one directory in the directory if the directory has only one entry that is
275/// the directory, otherwise returns the path to the directory.
276pub fn only_one_dir_in_dir<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>>
277{
278    let entries = read_dir(path)?;
279    let mut is_first = true;
280    let mut path_buf: Option<PathBuf> = None;
281    for entry in entries {
282        let tmp_entry = entry?;
283        if is_first && tmp_entry.metadata()?.is_dir() {
284            path_buf = Some(tmp_entry.path());
285        } else {
286            path_buf = None;
287        }
288        is_first = false;
289    }
290    Ok(path_buf)
291}
292
293#[cfg(test)]
294mod tests;