use crate::utils::fs::dirs::ensure_dir;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
pub fn safe_write(path: &Path, content: &str) -> Result<()> {
atomic_write(path, content.as_bytes())
}
pub fn atomic_write(path: &Path, content: &[u8]) -> Result<()> {
use std::io::Write;
let safe_path = crate::utils::platform::windows_long_path(path);
if let Some(parent) = safe_path.parent() {
ensure_dir(parent)?;
}
let temp_path = safe_path.with_extension("tmp");
{
let mut file = fs::File::create(&temp_path).with_context(|| {
let platform_help = if crate::utils::platform::is_windows() {
"On Windows: Check file permissions, path length, and that directory exists"
} else {
"Check file permissions and that directory exists"
};
format!("Failed to create temp file: {}\n\n{}", temp_path.display(), platform_help)
})?;
file.write_all(content)
.with_context(|| format!("Failed to write to temp file: {}", temp_path.display()))?;
file.sync_all().with_context(|| "Failed to sync file to disk")?;
}
fs::rename(&temp_path, &safe_path)
.with_context(|| format!("Failed to rename temp file to: {}", safe_path.display()))?;
Ok(())
}
pub async fn atomic_write_multiple(files: &[(std::path::PathBuf, Vec<u8>)]) -> Result<()> {
use futures::future::try_join_all;
if files.is_empty() {
return Ok(());
}
let mut tasks = Vec::new();
for (path, content) in files {
let path = path.clone();
let content = content.clone();
let task =
tokio::task::spawn_blocking(move || atomic_write(&path, &content).map(|()| path));
tasks.push(task);
}
let results = try_join_all(tasks).await.context("Failed to join atomic write tasks")?;
let mut errors = Vec::new();
for result in results {
if let Err(e) = result {
errors.push(e);
}
}
if !errors.is_empty() {
let error_msgs: Vec<String> =
errors.into_iter().map(|error| format!(" {error}")).collect();
return Err(anyhow::anyhow!(
"Failed to write {} files:\n{}",
error_msgs.len(),
error_msgs.join("\n")
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_safe_write() {
let temp = tempdir().unwrap();
let file_path = temp.path().join("test.txt");
safe_write(&file_path, "test content").unwrap();
let content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "test content");
}
#[test]
fn test_safe_write_creates_parent_dirs() {
let temp = tempdir().unwrap();
let file_path = temp.path().join("subdir").join("test.txt");
safe_write(&file_path, "test content").unwrap();
assert!(file_path.exists());
let content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "test content");
}
#[test]
fn test_atomic_write_basic() {
let temp = tempdir().unwrap();
let file = temp.path().join("atomic.txt");
atomic_write(&file, b"test content").unwrap();
assert_eq!(std::fs::read_to_string(&file).unwrap(), "test content");
}
#[test]
fn test_atomic_write_overwrites() {
let temp = tempdir().unwrap();
let file = temp.path().join("atomic.txt");
atomic_write(&file, b"initial").unwrap();
assert_eq!(std::fs::read_to_string(&file).unwrap(), "initial");
atomic_write(&file, b"updated").unwrap();
assert_eq!(std::fs::read_to_string(&file).unwrap(), "updated");
}
#[test]
fn test_atomic_write_creates_parent() {
let temp = tempdir().unwrap();
let file = temp.path().join("deep").join("nested").join("atomic.txt");
atomic_write(&file, b"nested content").unwrap();
assert!(file.exists());
assert_eq!(std::fs::read_to_string(&file).unwrap(), "nested content");
}
#[tokio::test]
async fn test_atomic_write_multiple() {
let temp = tempdir().unwrap();
let file1 = temp.path().join("atomic1.txt");
let file2 = temp.path().join("atomic2.txt");
let files =
vec![(file1.clone(), b"content1".to_vec()), (file2.clone(), b"content2".to_vec())];
atomic_write_multiple(&files).await.unwrap();
assert!(file1.exists());
assert!(file2.exists());
assert_eq!(std::fs::read_to_string(&file1).unwrap(), "content1");
assert_eq!(std::fs::read_to_string(&file2).unwrap(), "content2");
}
#[tokio::test]
async fn test_atomic_write_multiple_partial_failure() {
let temp = tempdir().unwrap();
let valid_path = temp.path().join("valid.txt");
let invalid_base = temp.path().join("not_a_directory.txt");
std::fs::write(&invalid_base, "this is a file").unwrap();
let invalid_path = invalid_base.join("impossible_file.txt");
let files =
vec![(valid_path.clone(), b"content".to_vec()), (invalid_path, b"fail".to_vec())];
let result = atomic_write_multiple(&files).await;
assert!(result.is_err());
}
#[test]
fn test_safe_write_readonly_parent() {
if std::env::var("CI").is_ok() {
return;
}
let temp = tempdir().unwrap();
let readonly_dir = temp.path().join("readonly");
ensure_dir(&readonly_dir).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&readonly_dir).unwrap().permissions();
perms.set_mode(0o555); std::fs::set_permissions(&readonly_dir, perms).unwrap();
let file = readonly_dir.join("test.txt");
let result = safe_write(&file, "test");
assert!(result.is_err());
let mut perms = std::fs::metadata(&readonly_dir).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&readonly_dir, perms).unwrap();
}
}
#[test]
fn test_safe_copy_file() {
let temp = tempdir().unwrap();
let src = temp.path().join("source.txt");
let dst = temp.path().join("dest.txt");
std::fs::write(&src, "test content").unwrap();
std::fs::copy(&src, &dst).unwrap();
assert_eq!(std::fs::read_to_string(&dst).unwrap(), "test content");
}
#[test]
fn test_copy_with_parent_creation() {
let temp = tempdir().unwrap();
let src = temp.path().join("source.txt");
let dst = temp.path().join("subdir").join("dest.txt");
std::fs::write(&src, "test content").unwrap();
crate::utils::fs::ensure_parent_dir(&dst).unwrap();
std::fs::copy(&src, &dst).unwrap();
assert!(dst.exists());
assert_eq!(std::fs::read_to_string(&dst).unwrap(), "test content");
}
#[test]
fn test_copy_nonexistent_source() {
let temp = tempdir().unwrap();
let src = temp.path().join("nonexistent.txt");
let dst = temp.path().join("dest.txt");
let result = std::fs::copy(&src, &dst);
assert!(result.is_err());
}
}