rust_script_ext/
fs.rs

1//! File wrapper with extra context on errors and buffered reading/writing.
2use crate::prelude::*;
3use std::{
4    fmt,
5    io::{self, BufWriter, Read, Seek, Write},
6    path::{Path, PathBuf},
7};
8
9/// Wraps a std [`File`](std::fs::File) which provides extra context for errors and buffered
10/// writing.
11#[derive(Debug)]
12pub struct File {
13    inner: BufWriter<std::fs::File>,
14    path: PathBuf,
15}
16
17impl File {
18    /// Opens a file in write-only mode.
19    ///
20    /// This function will create a file if it does not exist, and will truncate it if it does.
21    ///
22    /// **If the parent directory does not exist, it will be created.**
23    pub fn create(path: impl Into<PathBuf>) -> Result<Self> {
24        let path = path.into();
25        create_p_dir(&path);
26        let inner = std::fs::File::create(&path)
27            .with_context(|| format!("failed to create or open file '{}'", path.display()))
28            .map(BufWriter::new)?;
29
30        Ok(Self { path, inner })
31    }
32
33    /// Opens a file in write-only mode.
34    ///
35    /// This function will create a file if it does not exist, and will append to it if it does.
36    /// **If the parent directory does not exist, it will be created.**
37    pub fn append(path: impl Into<PathBuf>) -> Result<Self> {
38        let path = path.into();
39        create_p_dir(&path);
40        let inner = std::fs::File::options()
41            .create(true)
42            .append(true)
43            .open(&path)
44            .with_context(|| format!("failed to create or open file '{}'", path.display()))
45            .map(BufWriter::new)?;
46
47        Ok(Self { path, inner })
48    }
49
50    /// Opens a file in read-only mode.
51    pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
52        let path = path.into();
53        let inner = std::fs::File::open(&path)
54            .with_context(|| format!("failed to open file '{}'", path.display()))
55            .map(BufWriter::new)?;
56
57        Ok(Self { path, inner })
58    }
59
60    /// The file path.
61    pub fn path(&self) -> &Path {
62        &self.path
63    }
64
65    /// Helper for `std::path::Path::new(path).exists()`.
66    pub fn exists(path: impl AsRef<Path>) -> bool {
67        path.as_ref().exists()
68    }
69
70    /// Unwrap into `std::fs::File`, flushing any data to be written.
71    pub fn into_std_file(self) -> Result<std::fs::File> {
72        self.inner.into_inner().map_err(Into::into)
73    }
74
75    /// Read entire file contents to byte buffer.
76    ///
77    /// Note that reading starts from where the cursor is.
78    /// Previous reads may have advanced the cursor.
79    pub fn read_to_vec(&mut self) -> Result<Vec<u8>> {
80        let len = self
81            .inner
82            .get_ref()
83            .metadata()
84            .map(|x| x.len())
85            .unwrap_or_default() as usize;
86        let mut buf = Vec::with_capacity(len);
87        self.read_to_end(&mut buf)
88            .with_context(|| format!("failed reading bytes from '{}'", self.path.display()))?;
89        Ok(buf)
90    }
91
92    /// Read entire file contents as a UTF8 encoded string.
93    ///
94    /// Note that reading starts from where the cursor is.
95    /// Previous reads may have advanced the cursor.
96    pub fn read_to_string(&mut self) -> Result<String> {
97        self.read_to_vec().and_then(|x| {
98            String::from_utf8(x).with_context(|| {
99                format!(
100                    "failed to encode bytes from '{}' as UTF8",
101                    self.path.display()
102                )
103            })
104        })
105    }
106
107    /// Conveniance function to write bytes to the file.
108    pub fn write(&mut self, contents: impl AsRef<[u8]>) -> Result<()> {
109        self.write_all(contents.as_ref())
110            .with_context(|| format!("failed to write to '{}'", self.path.display()))
111    }
112
113    fn wrap_err(&self, err: io::Error) -> io::Error {
114        let kind = err.kind();
115        io::Error::new(
116            kind,
117            Error {
118                path: self.path.clone(),
119                inner: err,
120            },
121        )
122    }
123}
124
125fn create_p_dir(path: &Path) {
126    if let Some(p) = path.parent() {
127        if let Err(e) = std::fs::create_dir_all(p) {
128            eprintln!("failed to create parent directory '{}': {e}", p.display());
129        }
130    }
131}
132
133#[derive(Debug)]
134struct Error {
135    path: PathBuf,
136    inner: io::Error,
137}
138
139impl std::error::Error for Error {
140    fn cause(&self) -> Option<&dyn std::error::Error> {
141        Some(&self.inner)
142    }
143
144    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
145        Some(&self.inner)
146    }
147}
148
149impl fmt::Display for Error {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        write!(
152            f,
153            "io error with file '{}': {}",
154            self.path.display(),
155            self.inner
156        )
157    }
158}
159
160impl Read for File {
161    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
162        self.inner.get_mut().read(buf).map_err(|e| self.wrap_err(e))
163    }
164
165    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
166        self.inner
167            .get_mut()
168            .read_exact(buf)
169            .map_err(|e| self.wrap_err(e))
170    }
171
172    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
173        self.inner
174            .get_mut()
175            .read_to_end(buf)
176            .map_err(|e| self.wrap_err(e))
177    }
178
179    fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
180        self.inner
181            .get_mut()
182            .read_vectored(bufs)
183            .map_err(|e| self.wrap_err(e))
184    }
185
186    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
187        self.inner
188            .get_mut()
189            .read_to_string(buf)
190            .map_err(|e| self.wrap_err(e))
191    }
192}
193
194impl Write for File {
195    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
196        self.inner.write(buf).map_err(|e| self.wrap_err(e))
197    }
198
199    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
200        self.inner
201            .write_vectored(bufs)
202            .map_err(|e| self.wrap_err(e))
203    }
204
205    fn flush(&mut self) -> io::Result<()> {
206        self.inner.flush().map_err(|e| self.wrap_err(e))
207    }
208}
209
210impl Seek for File {
211    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
212        self.inner.seek(pos).map_err(|e| self.wrap_err(e))
213    }
214
215    fn stream_position(&mut self) -> io::Result<u64> {
216        self.inner.stream_position().map_err(|e| self.wrap_err(e))
217    }
218}
219
220/// List out the directory entries under `path` which match the **glob** pattern `matching`.
221///
222/// The return `PathBuf`s will have `path` prefixed.
223///
224/// # Example
225/// ```rust
226/// # use rust_script_ext::prelude::*;
227/// # use std::path::PathBuf;
228/// let ps = ls("src", "*.rs").unwrap();
229/// assert_eq!(ps, vec![
230///     PathBuf::from("src/args.rs"),
231///     PathBuf::from("src/cmd.rs"),
232///     PathBuf::from("src/fs.rs"),
233///     PathBuf::from("src/io.rs"),
234///     PathBuf::from("src/lib.rs"),
235/// ]);
236/// ```
237pub fn ls<P, M>(path: P, matching: M) -> Result<Vec<PathBuf>>
238where
239    P: AsRef<Path>,
240    M: AsRef<str>,
241{
242    let pat = matching.as_ref();
243    let glob = globset::Glob::new(pat)
244        .with_context(|| format!("invalid glob pattern: {pat}"))?
245        .compile_matcher();
246
247    let prefix = path.as_ref();
248    let rdr = std::fs::read_dir(prefix).with_context(|| {
249        format!(
250            "failed to read directory: {}",
251            prefix
252                .canonicalize()
253                .unwrap_or_else(|_| prefix.to_path_buf())
254                .display()
255        )
256    })?;
257
258    let mut v = Vec::new();
259    for e in rdr {
260        let e = e.with_context(|| {
261            format!(
262                "failed to read directory: {}",
263                prefix
264                    .canonicalize()
265                    .unwrap_or_else(|_| prefix.to_path_buf())
266                    .display()
267            )
268        })?;
269
270        let path = e.path();
271
272        if glob.is_match(path.strip_prefix(prefix).expect("path prefix matches")) {
273            v.push(path);
274        }
275    }
276
277    v.sort_unstable();
278
279    Ok(v)
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn file_not_found() {
288        let x = File::open("wont-exist.txt").unwrap_err().to_string();
289        assert_eq!(&x, "failed to open file 'wont-exist.txt'");
290    }
291}