Skip to main content

ito_common/
io.rs

1//! Convenience I/O helpers.
2//!
3//! These functions wrap common `std::fs` operations and attach contextual error
4//! messages (including the failing path) where appropriate.
5
6use std::io::ErrorKind;
7use std::path::Path;
8
9use miette::{Result, miette};
10
11/// Read a file into a UTF-8 string.
12///
13/// On error, the returned diagnostic includes the failing path.
14pub fn read_to_string(path: &Path) -> Result<String> {
15    std::fs::read_to_string(path)
16        .map_err(|e| miette!("I/O error reading {p}: {e}", p = path.display()))
17}
18
19/// Read a file into a UTF-8 string.
20///
21/// Missing files map to `Ok(None)`; other I/O errors are surfaced.
22pub fn read_to_string_optional(path: &Path) -> Result<Option<String>> {
23    match std::fs::read_to_string(path) {
24        Ok(s) => Ok(Some(s)),
25        Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
26        Err(e) => Err(miette!("I/O error reading {p}: {e}", p = path.display())),
27    }
28}
29
30/// Read a file into a string, returning an empty string on error.
31///
32/// This is intentionally lossy and should be used for best-effort output only.
33pub fn read_to_string_or_default(path: &Path) -> String {
34    std::fs::read_to_string(path).unwrap_or_default()
35}
36
37/// Write bytes to a file.
38///
39/// On error, the returned diagnostic includes the failing path.
40pub fn write(path: &Path, contents: impl AsRef<[u8]>) -> Result<()> {
41    std::fs::write(path, contents)
42        .map_err(|e| miette!("I/O error writing {p}: {e}", p = path.display()))
43}
44
45/// Create a directory and any missing parent directories.
46///
47/// On error, the returned diagnostic includes the failing path.
48pub fn create_dir_all(path: &Path) -> Result<()> {
49    std::fs::create_dir_all(path)
50        .map_err(|e| miette!("I/O error creating {p}: {e}", p = path.display()))
51}
52
53/// `std::io` variant of [`read_to_string()`].
54pub fn read_to_string_std(path: &Path) -> std::io::Result<String> {
55    std::fs::read_to_string(path)
56}
57
58/// `std::io` variant of [`write()`].
59pub fn write_std(path: &Path, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
60    std::fs::write(path, contents)
61}
62
63/// `std::io` variant of [`create_dir_all`].
64pub fn create_dir_all_std(path: &Path) -> std::io::Result<()> {
65    std::fs::create_dir_all(path)
66}
67
68/// Write bytes to `path` atomically, best-effort.
69///
70/// The write happens via a temporary file in the same directory followed by a
71/// rename.
72pub fn write_atomic_std(path: &Path, contents: impl AsRef<[u8]>) -> std::io::Result<()> {
73    let Some(parent) = path.parent() else {
74        return std::fs::write(path, contents);
75    };
76    std::fs::create_dir_all(parent)?;
77
78    let file_name = path
79        .file_name()
80        .map(|s| s.to_string_lossy().to_string())
81        .unwrap_or_else(|| "config".to_string());
82    let tmp_name = format!(".{file_name}.tmp.{}", std::process::id());
83    let tmp_path = parent.join(tmp_name);
84
85    std::fs::write(&tmp_path, contents)?;
86
87    #[cfg(windows)]
88    {
89        let _ = std::fs::remove_file(path);
90    }
91
92    let r = std::fs::rename(&tmp_path, path);
93    if r.is_err() {
94        let _ = std::fs::remove_file(&tmp_path);
95    }
96    r
97}