ts_io/
fs.rs

1//! Wrappers over parts of [`fs`] to return user friendly errors.
2
3use std::{
4    fs::{self},
5    io,
6    path::{Path, PathBuf},
7};
8
9use ts_error::error_enum;
10
11use crate::DisplayPath;
12
13/// Read a file, returning presentable error variants.
14pub fn read_file(path: &Path) -> Result<Vec<u8>, FileError> {
15    ensure_is_file(path)?;
16    fs::read(path).map_err(|source| FileError::read_error(path, source))
17}
18
19/// Read a file to a string, returning presentable error variants.
20pub fn read_file_to_string(path: &Path) -> Result<String, FileError> {
21    ensure_is_file(path)?;
22    fs::read_to_string(path).map_err(|source| FileError::read_error(path, source))
23}
24
25/// Return `Ok(())` if a path points to a file.
26pub fn ensure_is_file(path: &Path) -> Result<(), FileError> {
27    // Does not discriminate between directory or file.
28    if !fs::exists(path).map_err(|source| FileError::no_access(path, source))? {
29        return Err(FileError::does_not_exist(path));
30    }
31
32    // Traverses symbolic links, therefore, output should be either file or path.
33    let metadata = path
34        .metadata()
35        .map_err(|source| FileError::no_access(path, source))?;
36
37    if !metadata.is_file() {
38        return Err(FileError::not_a_file(path));
39    }
40
41    Ok(())
42}
43
44/// Error variants for interacting with a file.
45#[derive(Debug)]
46#[non_exhaustive]
47#[allow(missing_docs)]
48#[error_enum]
49pub enum FileError {
50    #[non_exhaustive]
51    DoesNotExist { path: PathBuf },
52
53    #[non_exhaustive]
54    NotAFile { path: PathBuf },
55
56    #[non_exhaustive]
57    NoAccess { path: PathBuf, source: io::Error },
58
59    #[non_exhaustive]
60    ReadError { path: PathBuf, source: io::Error },
61
62    #[non_exhaustive]
63    WriteError { path: PathBuf, source: io::Error },
64}
65impl core::fmt::Display for FileError {
66    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
67        match &self {
68            Self::DoesNotExist { path, .. } => {
69                write!(f, "`{}` does not exist", path.opinionated_display())
70            }
71            Self::NotAFile { path, .. } => {
72                write!(f, "`{}` is not a file", path.opinionated_display())
73            }
74            Self::NoAccess { path, .. } => {
75                write!(f, "could not access `{}`", path.opinionated_display())
76            }
77            Self::ReadError { path, .. } => {
78                write!(f, "could not read `{}`", path.opinionated_display())
79            }
80            Self::WriteError { path, .. } => {
81                write!(f, "could not write `{}`", path.opinionated_display())
82            }
83        }
84    }
85}
86impl core::error::Error for FileError {
87    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
88        match &self {
89            Self::NoAccess { source, .. }
90            | Self::WriteError { source, .. }
91            | Self::ReadError { source, .. } => Some(source),
92            _ => None,
93        }
94    }
95}