Skip to content
60 changes: 57 additions & 3 deletions crates/pixi_utils/src/atomic_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ fn temp_file_for(path: &Path) -> std::io::Result<tempfile::NamedTempFile> {
path.file_name().and_then(|n| n.to_str()).unwrap_or("tmp")
);

tempfile::Builder::new().prefix(&prefix).tempfile_in(dir)
match tempfile::Builder::new().prefix(&prefix).tempfile_in(dir) {
Ok(file) => Ok(file),
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => tempfile::Builder::new()
.prefix(&prefix)
.tempfile_in(std::env::temp_dir()),
Err(e) => Err(e),
}
}

/// Atomically write contents to a file by first writing to a temporary file in
/// the same directory and then renaming it to the target path.
/// Atomically write contents to a file by first writing to a temporary file and
/// then renaming it to the target path.
///
/// This ensures that the target file is never left in a partially-written state.
/// If the write fails (e.g., due to disk full), the original file remains
Expand Down Expand Up @@ -49,3 +55,51 @@ pub fn atomic_write_sync(path: &Path, contents: impl AsRef<[u8]>) -> std::io::Re

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;

#[test]
fn test_temp_file_created_in_same_dir_when_writable() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("pixi.toml");

let temp = temp_file_for(&target).unwrap();

assert_eq!(temp.path().parent().unwrap(), dir.path());
}

#[test]
#[cfg(unix)]
fn test_temp_file_falls_back_to_tmp_when_parent_not_writable() {
use std::os::unix::fs::PermissionsExt;

let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("pixi.toml");
fs::write(&target, b"[project]").unwrap();

fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o555)).unwrap();

let temp = temp_file_for(&target).unwrap();

assert_eq!(temp.path().parent().unwrap(), std::env::temp_dir());
// resetting the permissions for cleanup
fs::set_permissions(dir.path(), fs::Permissions::from_mode(0o755)).unwrap();
}

#[test]
fn temp_file_has_correct_prefix() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("pixi.toml");

let temp = temp_file_for(&target).unwrap();
let name = temp.path().file_name().unwrap().to_str().unwrap();

assert!(
name.starts_with(".pixi.toml."),
"expected prefix `.pixi.toml.`, got `{name}`"
);
}
}
Loading