camino_fs/
lib.rs

1mod fs;
2mod ls;
3
4use fs::*;
5use ls::Ls;
6use std::{collections::VecDeque, io, iter, path::Path, time::SystemTime};
7
8pub use camino::{Utf8Path, Utf8PathBuf};
9
10pub trait Utf8PathBufExt {
11    fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Utf8PathBuf>;
12}
13
14impl Utf8PathBufExt for Utf8PathBuf {
15    fn from_path<P: AsRef<Path>>(path: P) -> io::Result<Self> {
16        Utf8PathBuf::from_path_buf(path.as_ref().to_path_buf()).map_err(|e| {
17            io::Error::new(
18                io::ErrorKind::Other,
19                format!("Could not convert to pathbuf: {e:?}"),
20            )
21        })
22    }
23}
24
25pub trait Utf8PathExt {
26    /// Returns the path relative to the given base path.
27    ///
28    /// This is really just a wrapper around Utf8Path's `strip_prefix` method.
29    fn relative_to<P: AsRef<Path>>(&self, path: P) -> Option<&'_ Utf8Path>;
30
31    /// Add an extension to the path. If the path already has an extension, it is appended
32    /// with the new extension.
33    ///
34    /// Example:
35    ///
36    /// ```
37    /// use camino_fs::*;
38    ///
39    /// let path = Utf8Path::new("file.txt").join_ext("gz");
40    /// assert_eq!(path.all_extensions(), Some("txt.gz"));
41    /// ```
42    fn join_ext<S: AsRef<str>>(&self, ext: S) -> Utf8PathBuf;
43
44    /// Returns an iterator over the extensions of the path.
45    fn extensions<'a>(&'a self) -> Box<dyn Iterator<Item = &'a str> + 'a>;
46
47    /// Returns all extensions, i.e. the string after the first dot in the filename
48    fn all_extensions(&self) -> Option<&str>;
49
50    /// Returns an iterator over the entries in the directory (non recursively)
51    /// or an error if the path is not a directory.
52    ///
53    /// If there is an error getting the path of an entry, it is skipped.
54    ///
55    /// Note that this is not performance optimized and may be slow for large directories.
56    fn ls(&self) -> Ls;
57
58    /// Create directory if it does not exist.
59    fn mkdir(&self) -> io::Result<()>;
60
61    /// Create all directories if they don't exist.
62    fn mkdirs(&self) -> io::Result<()>;
63
64    /// Remove the file or directory at the path.
65    ///
66    /// Does nothing if the path does not exist.
67    fn rm(&self) -> io::Result<()>;
68
69    /// Remove all files and directories in the directory recursively that match the predicate.
70    fn rm_matching<P: Fn(&Utf8Path) -> bool>(&self, predicate: P) -> io::Result<()>;
71
72    /// Copy recursively from the path to the destination path.
73    fn cp<P: Into<Utf8PathBuf>>(&self, to: P) -> io::Result<()>;
74
75    /// Renames a file or directory to a new name, replacing the original file if to already exists.
76    fn mv<P: Into<Utf8PathBuf>>(&self, to: P) -> io::Result<()>;
77
78    /// Throw an error if the path does not exist.
79    fn assert_exists(&self) -> io::Result<()>;
80
81    /// Throw an error if the path is not a directory.
82    fn assert_dir(&self) -> io::Result<()>;
83
84    /// Throw an error if the path is not a file.
85    fn assert_file(&self) -> io::Result<()>;
86
87    /// Write to the file at the path. Creates the file if it does not exist
88    /// and replaces the content if it does.
89    ///
90    /// If the path also contains directories that do not exist, they will be created.
91    fn write<B: AsRef<[u8]>>(&self, buf: B) -> io::Result<()>;
92
93    /// Read a file
94    fn read_bytes(&self) -> io::Result<Vec<u8>>;
95
96    /// Read a file as a string
97    fn read_string(&self) -> io::Result<String>;
98
99    /// Get the system time for a file or folder
100    fn mtime(&self) -> Option<SystemTime>;
101}
102
103impl Utf8PathExt for Utf8Path {
104    fn assert_exists(&self) -> io::Result<()> {
105        if !self.exists() {
106            return Err(io::Error::new(
107                io::ErrorKind::NotFound,
108                format!("Path \"{}\" does not exist or you don't have access!", self),
109            ));
110        }
111        Ok(())
112    }
113
114    fn assert_dir(&self) -> io::Result<()> {
115        if !self.is_dir() {
116            return Err(io::Error::new(
117                io::ErrorKind::InvalidInput,
118                format!("Path \"{}\" is not a directory!", self),
119            ));
120        }
121        Ok(())
122    }
123
124    fn assert_file(&self) -> io::Result<()> {
125        if !self.is_file() {
126            return Err(io::Error::new(
127                io::ErrorKind::InvalidInput,
128                format!("Path \"{}\" is not a file!", self),
129            ));
130        }
131        Ok(())
132    }
133
134    fn cp<P: Into<Utf8PathBuf>>(&self, to: P) -> io::Result<()> {
135        self.assert_exists()?;
136        let dest = to.into();
137
138        if self.is_dir() {
139            self.assert_dir()?;
140
141            dest.mkdirs()?;
142
143            let mut entries: VecDeque<Utf8PathBuf> = self.ls().collect();
144
145            while let Some(src_path) = entries.pop_front() {
146                let rel_path = src_path.strip_prefix(self).unwrap();
147                let dest_path = dest.join(rel_path);
148
149                if src_path.is_dir() {
150                    entries.extend(src_path.ls());
151                    dest_path.mkdir()?;
152                } else {
153                    fs_copy(&src_path, &dest_path)?;
154                }
155            }
156        } else {
157            fs_copy(self, &dest)?;
158        }
159        Ok(())
160    }
161
162    fn mv<P: Into<Utf8PathBuf>>(&self, to: P) -> io::Result<()> {
163        self.assert_exists()?;
164        fs_rename(self, &to.into())
165    }
166
167    fn rm(&self) -> io::Result<()> {
168        if !self.exists() {
169            Ok(())
170        } else if self.is_dir() {
171            fs_remove_dir_all(self)
172        } else {
173            fs_remove_file(self)
174        }
175    }
176
177    fn rm_matching<P: Fn(&Utf8Path) -> bool>(&self, predicate: P) -> io::Result<()> {
178        if self.is_dir() {
179            for file in self.ls().filter(|p| predicate(p)) {
180                file.rm()?;
181            }
182        } else if predicate(self) {
183            self.rm()?;
184        }
185        Ok(())
186    }
187
188    fn mkdir(&self) -> io::Result<()> {
189        if !self.exists() {
190            fs_create_dir(self)?;
191        }
192        Ok(())
193    }
194
195    fn mkdirs(&self) -> io::Result<()> {
196        fs_create_dir_all(self)
197    }
198
199    fn ls(&self) -> Ls {
200        Ls::new(self.to_path_buf())
201    }
202
203    fn extensions<'a>(&'a self) -> Box<dyn Iterator<Item = &'a str> + 'a> {
204        if let Some(name) = self.file_name() {
205            Box::new(name.split('.').take(1))
206        } else {
207            Box::new(iter::empty())
208        }
209    }
210
211    fn join_ext<S: AsRef<str>>(&self, ext: S) -> Utf8PathBuf {
212        let ext = ext.as_ref();
213        let mut s = self.to_string();
214        if !ext.starts_with('.') {
215            s.push('.');
216        }
217        s.push_str(ext);
218        Utf8PathBuf::from(s)
219    }
220
221    fn all_extensions(&self) -> Option<&str> {
222        Some(self.file_name()?.split_once('.')?.1)
223    }
224
225    fn relative_to<P: AsRef<Path>>(&self, path: P) -> Option<&'_ Utf8Path> {
226        self.strip_prefix(path).ok()
227    }
228
229    fn write<B: AsRef<[u8]>>(&self, buf: B) -> io::Result<()> {
230        if let Some(parent) = self.parent() {
231            parent.mkdirs()?;
232        }
233        fs_write(self, buf.as_ref())
234    }
235
236    fn read_bytes(&self) -> io::Result<Vec<u8>> {
237        fs_read(self)
238    }
239
240    fn read_string(&self) -> io::Result<String> {
241        fs_read_to_string(self)
242    }
243
244    fn mtime(&self) -> Option<SystemTime> {
245        self.metadata().ok().map(|md| md.modified().unwrap())
246    }
247}