tmc_langs_util/
file_util.rs

1//! Various utility functions, primarily wrapping the standard library's IO and filesystem functions
2
3#[cfg(unix)]
4mod lock_unix;
5#[cfg(windows)]
6mod lock_windows;
7
8use crate::error::FileError;
9#[cfg(unix)]
10pub use lock_unix::*;
11#[cfg(windows)]
12pub use lock_windows::*;
13use std::{
14    fs::{self, File, OpenOptions, ReadDir},
15    io::{Read, Write},
16    path::{Path, PathBuf},
17};
18use tempfile::NamedTempFile;
19use walkdir::WalkDir;
20
21pub const LOCK_FILE_NAME: &str = ".tmc.lock";
22
23#[derive(Debug, Clone, Copy)]
24pub enum LockOptions {
25    /// Shared read lock
26    Read,
27    /// Shared read lock, create file if it doesn't exist instead of erroring (including intermediate directories)
28    ReadCreate,
29    /// Shared write lock, create file if it doesn't exist, truncate if it does
30    ReadTruncate,
31    /// Exclusive write lock
32    Write,
33    /// Exclusive write lock, create file if it doesn't exist instead of erroring (including intermediate directories)
34    WriteCreate,
35    /// Exclusive write lock, create file if it doesn't exist, truncate if it does
36    WriteTruncate,
37}
38
39impl LockOptions {
40    fn into_open_options(self) -> OpenOptions {
41        let mut opts = OpenOptions::new();
42        match self {
43            Self::Read => opts.read(true),
44            // create requires write
45            Self::ReadCreate => opts.read(true).write(true).create(true),
46            // truncate requires write
47            Self::ReadTruncate => opts.write(true).create(true).truncate(true),
48            Self::Write => opts.write(true),
49            Self::WriteCreate => opts.write(true).create(true),
50            Self::WriteTruncate => opts.write(true).create(true).truncate(true),
51        };
52        opts
53    }
54}
55
56pub fn temp_file() -> Result<File, FileError> {
57    tempfile::tempfile().map_err(FileError::TempFile)
58}
59
60pub fn named_temp_file() -> Result<NamedTempFile, FileError> {
61    tempfile::NamedTempFile::new().map_err(FileError::TempFile)
62}
63
64pub fn named_temp_file_in(path: &Path) -> Result<NamedTempFile, FileError> {
65    tempfile::NamedTempFile::new_in(path).map_err(FileError::TempFile)
66}
67
68pub fn open_file(path: impl AsRef<Path>) -> Result<File, FileError> {
69    let path = path.as_ref();
70    File::open(path).map_err(|e| FileError::FileOpen(path.to_path_buf(), e))
71}
72
73pub fn read_reader<R: Read>(mut reader: R) -> Result<Vec<u8>, FileError> {
74    let mut bytes = vec![];
75    reader
76        .read_to_end(&mut bytes)
77        .map_err(FileError::ReadError)?;
78    Ok(bytes)
79}
80
81pub fn read_file<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, FileError> {
82    let path = path.as_ref();
83    let mut file = open_file(path)?;
84    let mut bytes = vec![];
85    file.read_to_end(&mut bytes)
86        .map_err(|e| FileError::FileRead(path.to_path_buf(), e))?;
87    Ok(bytes)
88}
89
90pub fn read_file_to_string<P: AsRef<Path>>(path: P) -> Result<String, FileError> {
91    let path = path.as_ref();
92    let s = fs::read_to_string(path).map_err(|e| FileError::FileRead(path.to_path_buf(), e))?;
93    Ok(s)
94}
95
96pub fn read_file_to_string_lossy<P: AsRef<Path>>(path: P) -> Result<String, FileError> {
97    let path = path.as_ref();
98    let bytes = read_file(path)?;
99    let s = String::from_utf8_lossy(&bytes).into_owned();
100    Ok(s)
101}
102
103/// Note: creates all intermediary directories if needed.
104pub fn create_file<P: AsRef<Path>>(path: P) -> Result<File, FileError> {
105    let path = path.as_ref();
106    if let Some(parent) = path.parent() {
107        if !parent.exists() {
108            create_dir_all(parent)?;
109        }
110    }
111    File::create(path).map_err(|e| FileError::FileCreate(path.to_path_buf(), e))
112}
113
114/// Removes whatever is at the path, whether it is a directory or file. The _all suffix hopefully makes the function sound at least slightly dangerous.
115pub fn remove_all<P: AsRef<Path>>(path: P) -> Result<(), FileError> {
116    let path = path.as_ref();
117    if path.is_file() {
118        remove_file(path)
119    } else if path.is_dir() {
120        remove_dir_all(path)
121    } else {
122        Ok(())
123    }
124}
125
126pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<(), FileError> {
127    let path = path.as_ref();
128    fs::remove_file(path).map_err(|e| FileError::FileRemove(path.to_path_buf(), e))
129}
130
131pub fn remove_file_locked<P: AsRef<Path>>(path: P) -> Result<(), FileError> {
132    let path = path.as_ref();
133    let _lock = Lock::file(path, LockOptions::Write)?;
134    fs::remove_file(path).map_err(|e| FileError::FileRemove(path.to_path_buf(), e))
135}
136
137pub fn write_to_file<S: AsRef<[u8]>, P: AsRef<Path>>(
138    source: S,
139    target: P,
140) -> Result<File, FileError> {
141    let target = target.as_ref();
142    let mut target_file = create_file(target)?;
143    target_file
144        .write_all(source.as_ref())
145        .map_err(|e| FileError::FileWrite(target.to_path_buf(), e))?;
146    Ok(target_file)
147}
148
149pub fn write_to_writer<S: AsRef<[u8]>, W: Write>(
150    source: S,
151    mut target: W,
152) -> Result<(), FileError> {
153    target
154        .write_all(source.as_ref())
155        .map_err(FileError::WriteError)?;
156    Ok(())
157}
158
159/// Reads all of the data from source and writes it into a new file at target.
160pub fn read_to_file<R: Read, P: AsRef<Path>>(source: &mut R, target: P) -> Result<File, FileError> {
161    let target = target.as_ref();
162    let mut target_file = create_file(target)?;
163    std::io::copy(source, &mut target_file)
164        .map_err(|e| FileError::FileWrite(target.to_path_buf(), e))?;
165    Ok(target_file)
166}
167
168pub fn read_dir<P: AsRef<Path>>(path: P) -> Result<ReadDir, FileError> {
169    fs::read_dir(&path).map_err(|e| FileError::DirRead(path.as_ref().to_path_buf(), e))
170}
171
172pub fn create_dir<P: AsRef<Path>>(path: P) -> Result<(), FileError> {
173    fs::create_dir(&path).map_err(|e| FileError::DirCreate(path.as_ref().to_path_buf(), e))
174}
175
176pub fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<(), FileError> {
177    fs::create_dir_all(&path).map_err(|e| FileError::DirCreate(path.as_ref().to_path_buf(), e))
178}
179
180pub fn remove_dir_empty<P: AsRef<Path>>(path: P) -> Result<(), FileError> {
181    fs::remove_dir(&path).map_err(|e| FileError::DirRemove(path.as_ref().to_path_buf(), e))
182}
183
184pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> Result<(), FileError> {
185    fs::remove_dir_all(&path).map_err(|e| FileError::DirRemove(path.as_ref().to_path_buf(), e))
186}
187
188pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<(), FileError> {
189    let from = from.as_ref();
190    let to = to.as_ref();
191    fs::rename(from, to).map_err(|e| FileError::Rename {
192        from: from.to_path_buf(),
193        to: to.to_path_buf(),
194        source: e,
195    })
196}
197
198/// Copies the file or directory at source into the target path.
199/// If the source is a file and the target is not a directory, the source file is copied to the target path.
200/// If the source is a file and the target is a directory, the source file is copied into the target directory.
201/// If the source is a directory and the target is not a file, the source directory and all files in it are copied recursively into the target directory. For example, with source=dir1 and target=dir2, dir1/file would be copied to dir2/dir1/file.
202/// If the source is a directory and the target is a file, an error is returned.
203pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(source: P, target: Q) -> Result<(), FileError> {
204    let source = source.as_ref();
205    let target = target.as_ref();
206
207    if source.is_file() {
208        if target.is_dir() {
209            log::trace!(
210                "copying into dir {} -> {}",
211                source.display(),
212                target.display()
213            );
214            let file_name = if let Some(file_name) = source.file_name() {
215                file_name
216            } else {
217                return Err(FileError::NoFileName(source.to_path_buf()));
218            };
219            let path_in_target = target.join(file_name);
220            std::fs::copy(source, path_in_target).map_err(|e| FileError::FileCopy {
221                from: source.to_path_buf(),
222                to: target.to_path_buf(),
223                source: e,
224            })?;
225        } else {
226            log::trace!("copying file {} -> {}", source.display(), target.display());
227            if let Some(parent) = target.parent() {
228                if !parent.exists() {
229                    create_dir_all(parent)?;
230                }
231            }
232            std::fs::copy(source, target).map_err(|e| FileError::FileCopy {
233                from: source.to_path_buf(),
234                to: target.to_path_buf(),
235                source: e,
236            })?;
237        }
238    } else {
239        log::trace!(
240            "recursively copying {} -> {}",
241            source.display(),
242            target.display()
243        );
244        if target.is_file() {
245            return Err(FileError::UnexpectedFile(target.to_path_buf()));
246        } else {
247            let prefix = source.parent().unwrap_or_else(|| Path::new(""));
248            for entry in WalkDir::new(source) {
249                let entry = entry?;
250                let entry_path = entry.path();
251                let stripped = entry_path
252                    .strip_prefix(prefix)
253                    .expect("prefix is derived from the source which entry_path is in");
254
255                let target = target.join(stripped);
256                if entry_path.is_dir() {
257                    create_dir_all(target)?;
258                } else {
259                    if let Some(parent) = target.parent() {
260                        create_dir_all(parent)?;
261                    }
262                    std::fs::copy(entry_path, &target).map_err(|e| FileError::FileCopy {
263                        from: entry_path.to_path_buf(),
264                        to: target.clone(),
265                        source: e,
266                    })?;
267                }
268            }
269        }
270    }
271    Ok(())
272}
273
274pub fn canonicalize(path: &Path) -> Result<PathBuf, FileError> {
275    let canon =
276        dunce::canonicalize(path).map_err(|e| FileError::Canonicalize(path.to_path_buf(), e))?;
277    Ok(canon)
278}
279
280#[cfg(test)]
281#[allow(clippy::unwrap_used)]
282mod test {
283    use super::*;
284    use std::path::PathBuf;
285
286    fn init() {
287        use log::*;
288        use simple_logger::*;
289        let _ = SimpleLogger::new().with_level(LevelFilter::Debug).init();
290    }
291
292    fn file_to(
293        target_dir: impl AsRef<std::path::Path>,
294        target_relative: impl AsRef<std::path::Path>,
295        contents: impl AsRef<[u8]>,
296    ) -> PathBuf {
297        let target = target_dir.as_ref().join(target_relative);
298        if let Some(parent) = target.parent() {
299            std::fs::create_dir_all(parent).unwrap();
300        }
301        std::fs::write(&target, contents.as_ref()).unwrap();
302        target
303    }
304
305    fn dir_to(
306        target_dir: impl AsRef<std::path::Path>,
307        target_relative: impl AsRef<std::path::Path>,
308    ) -> PathBuf {
309        let target = target_dir.as_ref().join(target_relative);
310        std::fs::create_dir_all(&target).unwrap();
311        target
312    }
313
314    #[test]
315    fn copies_file_to_file() {
316        init();
317
318        let temp = tempfile::tempdir().unwrap();
319        file_to(&temp, "dir/file", "file contents");
320
321        let target = tempfile::tempdir().unwrap();
322        copy(
323            temp.path().join("dir/file"),
324            target.path().join("another/place"),
325        )
326        .unwrap();
327
328        let conts = read_file_to_string(target.path().join("another/place")).unwrap();
329        assert_eq!(conts, "file contents");
330    }
331
332    #[test]
333    fn copies_file_to_dir() {
334        init();
335
336        let temp = tempfile::tempdir().unwrap();
337        file_to(&temp, "dir/file", "file contents");
338
339        let target = tempfile::tempdir().unwrap();
340        dir_to(&target, "some/dir");
341        copy(temp.path().join("dir/file"), target.path().join("some/dir")).unwrap();
342
343        let conts = read_file_to_string(target.path().join("some/dir/file")).unwrap();
344        assert_eq!(conts, "file contents");
345    }
346
347    #[test]
348    fn copies_dir() {
349        init();
350        let temp = tempfile::tempdir().unwrap();
351        file_to(&temp, "dir/another/file", "file contents");
352        file_to(&temp, "dir/elsewhere/f", "another file");
353        dir_to(&temp, "dir/some dir");
354
355        let target = tempfile::tempdir().unwrap();
356        copy(temp.path().join("dir"), target.path()).unwrap();
357
358        let conts = read_file_to_string(target.path().join("dir/another/file")).unwrap();
359        assert_eq!(conts, "file contents");
360        let conts = read_file_to_string(target.path().join("dir/elsewhere/f")).unwrap();
361        assert_eq!(conts, "another file");
362        assert!(target.path().join("dir/some dir").is_dir());
363    }
364}