use std::io::Write;
use std::path::{Path, PathBuf};
fn temp_path(path: &Path) -> PathBuf {
let dir = path.parent().unwrap_or_else(|| Path::new("."));
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "velra".into());
dir.join(format!(".{name}.velra-tmp-{}", std::process::id()))
}
pub fn write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
if let Some(dir) = path.parent() {
if !dir.as_os_str().is_empty() && !dir.is_dir() {
crate::home::ensure_dir(dir)?;
}
}
let tmp = temp_path(path);
{
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut f = opts.open(&tmp)?;
f.write_all(bytes)?;
f.flush()?;
f.sync_all()?;
}
#[cfg(unix)]
if let Ok(meta) = std::fs::metadata(path) {
let _ = std::fs::set_permissions(&tmp, meta.permissions());
}
match std::fs::rename(&tmp, path) {
Ok(()) => {}
Err(e) => {
let _ = std::fs::remove_file(&tmp);
return Err(e);
}
}
#[cfg(unix)]
if let Some(dir) = path.parent() {
if let Ok(d) = std::fs::File::open(dir) {
let _ = d.sync_all();
}
}
Ok(())
}
pub fn write_synced(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
if let Some(dir) = path.parent() {
if !dir.is_dir() {
crate::home::ensure_dir(dir)?;
}
}
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut f = opts.open(path)?;
f.write_all(bytes)?;
f.sync_all()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replaces_in_place() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("settings.json");
write(&p, b"{}\n").unwrap();
assert_eq!(std::fs::read(&p).unwrap(), b"{}\n");
write(&p, b"{\"a\":1}").unwrap();
assert_eq!(std::fs::read_to_string(&p).unwrap(), "{\"a\":1}");
let leftovers: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains("velra-tmp"))
.collect();
assert!(leftovers.is_empty());
}
}