Skip to main content

wist_shared/
fs.rs

1//! Shared filesystem helpers.
2
3use std::fs::{self, File};
4use std::io::{self, Write};
5use std::path::Path;
6
7use serde::Serialize;
8use serde::de::DeserializeOwned;
9
10pub fn ensure_parent(path: &Path) -> io::Result<()> {
11    if let Some(parent) = path.parent() {
12        fs::create_dir_all(parent)?;
13    }
14    Ok(())
15}
16
17pub fn read_json<T>(path: &Path) -> io::Result<T>
18where
19    T: DeserializeOwned,
20{
21    let text = fs::read_to_string(path)?;
22    serde_json::from_str(&text).map_err(io::Error::other)
23}
24
25pub fn write_json_atomic<T>(path: &Path, value: &T) -> io::Result<()>
26where
27    T: Serialize,
28{
29    let bytes = serde_json::to_vec_pretty(value).map_err(io::Error::other)?;
30    write_bytes_atomic(path, &bytes)
31}
32
33pub fn write_json_private_atomic<T>(path: &Path, value: &T) -> io::Result<()>
34where
35    T: Serialize,
36{
37    let bytes = serde_json::to_vec_pretty(value).map_err(io::Error::other)?;
38    write_bytes_private_atomic(path, &bytes)
39}
40
41pub fn write_json_compact_atomic<T>(path: &Path, value: &T) -> io::Result<()>
42where
43    T: Serialize,
44{
45    let bytes = serde_json::to_vec(value).map_err(io::Error::other)?;
46    write_bytes_atomic(path, &bytes)
47}
48
49pub fn write_bytes_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
50    ensure_parent(path)?;
51
52    let tmp_path = path.with_extension("tmp");
53    let mut file = File::create(&tmp_path)?;
54    file.write_all(bytes)?;
55    file.write_all(b"\n")?;
56    file.sync_all()?;
57    drop(file);
58
59    fs::rename(&tmp_path, path)?;
60    sync_parent_dir(path)?;
61    Ok(())
62}
63
64#[cfg(unix)]
65pub fn write_bytes_private_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
66    use std::fs::OpenOptions;
67    use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
68
69    ensure_parent(path)?;
70    if let Some(parent) = path.parent() {
71        fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
72    }
73
74    let tmp_path = path.with_extension("tmp");
75    let mut file = OpenOptions::new()
76        .create(true)
77        .truncate(true)
78        .write(true)
79        .mode(0o600)
80        .open(&tmp_path)?;
81    file.write_all(bytes)?;
82    file.write_all(b"\n")?;
83    file.sync_all()?;
84    drop(file);
85
86    fs::rename(&tmp_path, path)?;
87    fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
88    sync_parent_dir(path)?;
89    Ok(())
90}
91
92#[cfg(not(unix))]
93pub fn write_bytes_private_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
94    write_bytes_atomic(path, bytes)
95}
96
97#[cfg(unix)]
98fn sync_parent_dir(path: &Path) -> io::Result<()> {
99    if let Some(parent) = path.parent() {
100        File::open(parent)?.sync_all()?;
101    }
102    Ok(())
103}
104
105#[cfg(not(unix))]
106fn sync_parent_dir(_path: &Path) -> io::Result<()> {
107    Ok(())
108}