use std::fs::{self, File};
use std::io::{self, Write};
use std::path::Path;
use serde::Serialize;
use serde::de::DeserializeOwned;
pub fn ensure_parent(path: &Path) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
Ok(())
}
pub fn read_json<T>(path: &Path) -> io::Result<T>
where
T: DeserializeOwned,
{
let text = fs::read_to_string(path)?;
serde_json::from_str(&text).map_err(io::Error::other)
}
pub fn write_json_atomic<T>(path: &Path, value: &T) -> io::Result<()>
where
T: Serialize,
{
let bytes = serde_json::to_vec_pretty(value).map_err(io::Error::other)?;
write_bytes_atomic(path, &bytes)
}
pub fn write_json_private_atomic<T>(path: &Path, value: &T) -> io::Result<()>
where
T: Serialize,
{
let bytes = serde_json::to_vec_pretty(value).map_err(io::Error::other)?;
write_bytes_private_atomic(path, &bytes)
}
pub fn write_json_compact_atomic<T>(path: &Path, value: &T) -> io::Result<()>
where
T: Serialize,
{
let bytes = serde_json::to_vec(value).map_err(io::Error::other)?;
write_bytes_atomic(path, &bytes)
}
pub fn write_bytes_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
ensure_parent(path)?;
let tmp_path = path.with_extension("tmp");
let mut file = File::create(&tmp_path)?;
file.write_all(bytes)?;
file.write_all(b"\n")?;
file.sync_all()?;
drop(file);
fs::rename(&tmp_path, path)?;
sync_parent_dir(path)?;
Ok(())
}
#[cfg(unix)]
pub fn write_bytes_private_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
use std::fs::OpenOptions;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
ensure_parent(path)?;
if let Some(parent) = path.parent() {
fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
}
let tmp_path = path.with_extension("tmp");
let mut file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o600)
.open(&tmp_path)?;
file.write_all(bytes)?;
file.write_all(b"\n")?;
file.sync_all()?;
drop(file);
fs::rename(&tmp_path, path)?;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
sync_parent_dir(path)?;
Ok(())
}
#[cfg(not(unix))]
pub fn write_bytes_private_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
write_bytes_atomic(path, bytes)
}
#[cfg(unix)]
fn sync_parent_dir(path: &Path) -> io::Result<()> {
if let Some(parent) = path.parent() {
File::open(parent)?.sync_all()?;
}
Ok(())
}
#[cfg(not(unix))]
fn sync_parent_dir(_path: &Path) -> io::Result<()> {
Ok(())
}