use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const STALE_TEMP_AGE: Duration = Duration::from_secs(60);
pub(crate) fn resolve(path: &Path) -> PathBuf {
fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
fn temp_path_for(target: &Path) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut os = target.as_os_str().to_os_string();
os.push(format!(".tmp.{}.{nanos}", std::process::id()));
PathBuf::from(os)
}
fn sweep_stale_temps(target: &Path) {
let (Some(parent), Some(name)) = (target.parent(), target.file_name()) else {
return;
};
let parent = if parent.as_os_str().is_empty() {
Path::new(".")
} else {
parent
};
let mut prefix = name.to_os_string();
prefix.push(".tmp.");
let prefix = prefix.to_string_lossy().into_owned();
let Ok(entries) = fs::read_dir(parent) else {
return;
};
let now = SystemTime::now();
for entry in entries.flatten() {
if !entry.file_name().to_string_lossy().starts_with(&prefix) {
continue;
}
let stale = entry
.metadata()
.and_then(|m| m.modified())
.ok()
.and_then(|modified| now.duration_since(modified).ok())
.is_some_and(|age| age >= STALE_TEMP_AGE);
if stale {
let _ = fs::remove_file(entry.path());
}
}
}
struct TempFile {
path: PathBuf,
published: bool,
}
impl Drop for TempFile {
fn drop(&mut self) {
if !self.published {
let _ = fs::remove_file(&self.path);
}
}
}
#[cfg(unix)]
fn sync_parent(target: &Path) -> io::Result<()> {
use std::fs::File;
let parent = match target.parent() {
Some(p) if !p.as_os_str().is_empty() => p,
_ => Path::new("."),
};
match File::open(parent)?.sync_all() {
Err(e)
if matches!(
e.kind(),
io::ErrorKind::Unsupported | io::ErrorKind::InvalidInput
) =>
{
Ok(())
}
other => other,
}
}
#[cfg(not(unix))]
fn sync_parent(_target: &Path) -> io::Result<()> {
Ok(())
}
pub(crate) fn write_atomically(path: &Path, bytes: &[u8]) -> io::Result<()> {
let target = resolve(path);
sweep_stale_temps(&target);
let mut tmp = TempFile {
path: temp_path_for(&target),
published: false,
};
let mut opts = OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
{
let mut file = opts.open(&tmp.path)?;
file.write_all(bytes)?;
file.flush()?;
file.sync_all()?;
}
fs::rename(&tmp.path, &target)?;
tmp.published = true;
sync_parent(&target)
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_files_in(dir: &Path) -> Vec<PathBuf> {
fs::read_dir(dir)
.unwrap()
.map(|e| e.unwrap().path())
.filter(|p| p.to_string_lossy().contains(".tmp."))
.collect()
}
#[test]
fn temp_path_appends_to_the_full_name() {
let tmp = temp_path_for(Path::new("/h/.xurl.pending"));
assert!(
tmp.to_string_lossy().starts_with("/h/.xurl.pending.tmp."),
"{}",
tmp.display()
);
}
#[test]
fn two_temp_paths_for_one_target_differ() {
let a = temp_path_for(Path::new("/h/.xurl"));
let b = temp_path_for(Path::new("/h/.xurl"));
assert_ne!(a, b);
}
#[test]
fn a_stale_temp_file_is_swept_and_a_recent_one_is_left_alone() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("store");
let stale = dir.path().join("store.tmp.1.1");
let recent = dir.path().join("store.tmp.2.2");
fs::write(&stale, b"old secret").unwrap();
fs::write(&recent, b"live writer").unwrap();
fs::File::open(&stale)
.unwrap()
.set_modified(SystemTime::now() - Duration::from_secs(3600))
.unwrap();
write_atomically(&target, b"fresh").unwrap();
assert!(!stale.exists(), "a crashed writer's temp file is removed");
assert!(recent.exists(), "a temp file a live writer may own stays");
assert_eq!(fs::read(&target).unwrap(), b"fresh");
}
#[test]
fn a_failed_rename_leaves_no_temp_file_behind() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("occupied");
fs::create_dir(&target).unwrap();
let err = write_atomically(&target, b"secret").unwrap_err();
assert!(!err.to_string().is_empty());
assert!(
temp_files_in(dir.path()).is_empty(),
"the temp file must not survive a failed rename"
);
}
#[cfg(unix)]
#[test]
fn a_symlinked_target_is_written_through() {
let dir = tempfile::TempDir::new().unwrap();
let real = dir.path().join("real-store");
fs::write(&real, b"old").unwrap();
let link = dir.path().join("link-store");
std::os::unix::fs::symlink(&real, &link).unwrap();
write_atomically(&link, b"new").unwrap();
assert!(
fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink(),
"the link survives the write"
);
assert_eq!(fs::read(&real).unwrap(), b"new");
assert!(temp_files_in(dir.path()).is_empty());
}
}