Skip to main content

elph_core/
fs.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5use serde::Serialize;
6
7/// Create every directory in `dirs`, including parents.
8pub fn ensure_dirs(dirs: &[PathBuf]) -> Result<()> {
9    for dir in dirs {
10        fs::create_dir_all(dir).with_context(|| format!("failed to create {}", dir.display()))?;
11    }
12    Ok(())
13}
14
15/// Write a pretty-printed JSON file with mode `0600` on Unix.
16pub fn write_json_file<T: Serialize>(path: &Path, value: &T) -> Result<()> {
17    if let Some(parent) = path.parent() {
18        fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?;
19    }
20
21    let mut payload = serde_json::to_string_pretty(value).context("failed to serialize json")?;
22    payload.push('\n');
23    write_private_file(path, payload.as_bytes())
24}
25
26/// Write a private file with mode `0600` on Unix.
27pub fn write_private_file(path: &Path, contents: &[u8]) -> Result<()> {
28    #[cfg(unix)]
29    {
30        use std::fs::OpenOptions;
31        use std::io::Write;
32        use std::os::unix::fs::OpenOptionsExt;
33
34        let mut file = OpenOptions::new()
35            .write(true)
36            .create_new(true)
37            .mode(0o600)
38            .open(path)
39            .with_context(|| format!("failed to write {}", path.display()))?;
40        file.write_all(contents)?;
41        return Ok(());
42    }
43
44    #[cfg(not(unix))]
45    {
46        fs::write(path, contents).with_context(|| format!("failed to write {}", path.display()))?;
47        Ok(())
48    }
49}
50
51/// Write a file only when it does not already exist.
52pub fn write_file_if_missing(path: &Path, contents: &[u8]) -> Result<()> {
53    if path.exists() {
54        return Ok(());
55    }
56
57    if let Some(parent) = path.parent() {
58        fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?;
59    }
60
61    fs::write(path, contents).with_context(|| format!("failed to write {}", path.display()))?;
62    Ok(())
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn write_file_if_missing_is_idempotent() {
71        let tmp = tempfile::tempdir().expect("tempdir");
72        let path = tmp.path().join("marker.txt");
73
74        write_file_if_missing(&path, b"first").expect("first write");
75        write_file_if_missing(&path, b"second").expect("second write");
76
77        assert_eq!(fs::read_to_string(path).expect("read marker"), "first");
78    }
79}