up_rs/utils/
files.rs

1//! Utilities for dealing with files, including wrappers around `std::fs` APIs.
2
3use crate::errors::UpError;
4use crate::UP_BUNDLE_ID;
5use camino::Utf8Path;
6use camino::Utf8PathBuf;
7use color_eyre::eyre::eyre;
8use color_eyre::eyre::Context;
9use color_eyre::Result;
10use std::fs;
11use std::fs::File;
12use std::fs::OpenOptions;
13use std::os::unix::fs::OpenOptionsExt;
14use tracing::trace;
15use tracing::warn;
16
17/**
18Empty home directory. This is likely to cause issues as we expect to be able to create
19directories and files under the user's home directory, which this directory is used to deny.
20[More Info](https://serverfault.com/questions/116632/what-is-var-empty-and-why-is-this-directory-used-by-sshd)
21*/
22const EMPTY_HOME_DIR: &str = "/var/empty";
23
24/// Return path to user's home directory if we can discover it.
25pub fn home_dir() -> Result<Utf8PathBuf> {
26    let home_dir = dirs::home_dir()
27        .ok_or_else(|| eyre!("Expected to be able to calculate the user's home directory."))?;
28    let home_dir = Utf8PathBuf::try_from(home_dir)?;
29    if home_dir == EMPTY_HOME_DIR {
30        warn!(
31            "User home directory appears to be set to {EMPTY_HOME_DIR}. This is likely to cause \
32             issues with program execution."
33        );
34    }
35    Ok(home_dir)
36}
37
38/// The directory to which we write log files.
39pub fn log_dir() -> Result<Utf8PathBuf> {
40    Ok(home_dir()?.join("Library/Logs").join(UP_BUNDLE_ID))
41}
42
43/// Get a parent path or provide a useful error message.
44pub(crate) fn parent(path: &Utf8Path) -> Result<&Utf8Path> {
45    path.parent()
46        .ok_or_else(|| eyre!("Didn't expect path {path} to be the root directory."))
47}
48
49/// Convert a std path to a `Utf8Path`. We should be able to use `Utf8Path::try_from()`, but get
50/// compiler errors.
51pub fn to_utf8_path(path: &std::path::Path) -> Result<&Utf8Path> {
52    Utf8Path::from_path(path).ok_or_else(|| eyre!("Invalid UTF-8 in path {path:?}"))
53}
54
55/**
56Remove a broken symlink.
57
58You can normally check for a broken symlink with:
59`!path.exists() && path.symlink_metadata().is_ok()`
60This checks that the path pointed to doesn't exist, but that the symlink does exist.
61*/
62pub fn remove_broken_symlink(path: &Utf8Path) -> Result<(), UpError> {
63    warn!(
64        "Removing existing broken symlink.\n  Path: {path}\n  Dest: {dest}",
65        dest = &path.read_link_utf8().map_err(|e| UpError::IoError {
66            path: path.to_owned(),
67            source: e
68        })?
69    );
70    fs::remove_file(path).map_err(|e| UpError::DeleteError {
71        path: path.to_owned(),
72        source: e,
73    })?;
74
75    Ok(())
76}
77
78/// Ensure that a file exists with the specified permissions, creating it and its parent directories
79/// as needed.
80pub fn create(file_path: &Utf8Path, mode: Option<u32>) -> Result<File> {
81    trace!("Ensuring file exists: {file_path}");
82    create_dir_all(parent(file_path)?)?;
83    let mode = mode.unwrap_or(0o666);
84
85    let file = OpenOptions::new()
86        .write(true)
87        .create(true)
88        // This mode is only set if the file didn't exist.
89        .mode(mode)
90        .open(file_path)
91        .wrap_err_with(|| eyre!("Failed to create file at {file_path}"))?;
92
93    Ok(file)
94}
95
96/// Same as `std::fs::create_dir_all()` but with a better error message.
97pub fn create_dir_all(path: impl AsRef<Utf8Path>) -> Result<()> {
98    let path = path.as_ref();
99    trace!("Ensuring that directory path exists: {path}");
100    fs::create_dir_all(path).wrap_err_with(|| eyre!("Failed to create directory {path}"))
101}
102
103/// Same as [`std::fs::write()`] but with a better error message.
104pub(crate) fn write(path: impl AsRef<Utf8Path>, contents: impl AsRef<[u8]>) -> Result<()> {
105    let path = path.as_ref();
106    trace!("Writing data to {path}");
107    fs::write(path, contents).wrap_err_with(|| eyre!("Failed to write to file {path}"))
108}