atomcode_core/setup/
fs_atomic.rs1use anyhow::{Context, Result};
5use std::fs::File;
6use std::io::Write;
7use std::path::Path;
8
9pub fn atomic_write(path: &Path, content: &[u8], mode: u32) -> Result<()> {
21 let parent = path.parent().context("atomic_write: path has no parent")?;
22 std::fs::create_dir_all(parent)
23 .with_context(|| format!("atomic_write: create_dir_all({})", parent.display()))?;
24
25 let mut tmp = tempfile::NamedTempFile::new_in(parent)
26 .with_context(|| format!("atomic_write: NamedTempFile::new_in({})", parent.display()))?;
27 tmp.write_all(content)
28 .with_context(|| format!("atomic_write: write({})", tmp.path().display()))?;
29 tmp.as_file_mut()
30 .sync_all()
31 .with_context(|| format!("atomic_write: fsync({})", tmp.path().display()))?;
32
33 #[cfg(unix)]
34 {
35 use std::os::unix::fs::PermissionsExt;
36 std::fs::set_permissions(tmp.path(), std::fs::Permissions::from_mode(mode))
37 .with_context(|| format!("atomic_write: chmod({:o})", mode))?;
38 }
39 #[cfg(not(unix))]
40 {
41 let _ = mode;
42 }
48
49 tmp.persist(path)
50 .map_err(|e| anyhow::anyhow!("atomic_write: persist({}): {}", path.display(), e.error))?;
51
52 let dir = File::open(parent)
54 .with_context(|| format!("atomic_write: open parent dir {}", parent.display()))?;
55 dir.sync_all()
56 .with_context(|| format!("atomic_write: fsync parent {}", parent.display()))?;
57
58 Ok(())
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn atomic_write_creates_file_with_content() {
67 let dir = tempfile::tempdir().unwrap();
68 let path = dir.path().join("hello.txt");
69 atomic_write(&path, b"hello world", 0o644).unwrap();
70 let read = std::fs::read(&path).unwrap();
71 assert_eq!(read, b"hello world");
72 }
73
74 #[test]
75 fn atomic_write_overwrites_existing_file() {
76 let dir = tempfile::tempdir().unwrap();
77 let path = dir.path().join("file.txt");
78 std::fs::write(&path, b"original").unwrap();
79 atomic_write(&path, b"updated", 0o644).unwrap();
80 assert_eq!(std::fs::read(&path).unwrap(), b"updated");
81 }
82
83 #[cfg(unix)]
84 #[test]
85 fn atomic_write_sets_mode_0600_for_secrets() {
86 use std::os::unix::fs::PermissionsExt;
87 let dir = tempfile::tempdir().unwrap();
88 let path = dir.path().join("secret.json");
89 atomic_write(&path, b"{}", 0o600).unwrap();
90 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
91 assert_eq!(mode, 0o600);
92 }
93
94 #[test]
95 fn atomic_write_creates_parent_dirs() {
96 let dir = tempfile::tempdir().unwrap();
97 let path = dir.path().join("a/b/c/file.txt");
98 atomic_write(&path, b"deep", 0o644).unwrap();
99 assert_eq!(std::fs::read(&path).unwrap(), b"deep");
100 }
101}