Write text to a file via atomic sibling rename. Mirrors [`safe_write_json_file`] for generated prompt/rule files that are plain text rather than structured JSON. The target is not opened for writing until the final rename, so a failed write leaves the original untouched.
(path: &Path, contents: &str, backup: Option<&Path>)
| 662 | /// plain text rather than structured JSON. The target is not opened for writing |
| 663 | /// until the final rename, so a failed write leaves the original untouched. |
| 664 | pub fn safe_write_text_file(path: &Path, contents: &str, backup: Option<&Path>) -> Result<()> { |
| 665 | static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); |
| 666 | |
| 667 | if let Some(parent) = path.parent() { |
| 668 | std::fs::create_dir_all(parent).map_err(|e| TraceDecayError::Config { |
| 669 | message: format!("cannot create directory {}: {e}", parent.display()), |
| 670 | })?; |
| 671 | } |
| 672 | |
| 673 | let file_name = path |
| 674 | .file_name() |
| 675 | .and_then(|name| name.to_str()) |
| 676 | .unwrap_or("file"); |
| 677 | let unique = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); |
| 678 | let new_name = format!(".{file_name}.{}.{}.new", std::process::id(), unique); |
| 679 | let new_path = path |
| 680 | .parent() |
| 681 | .map_or_else(|| PathBuf::from(&new_name), |parent| parent.join(&new_name)); |
| 682 | if let Err(e) = std::fs::write(&new_path, contents) { |
| 683 | std::fs::remove_file(&new_path).ok(); |
| 684 | return Err(TraceDecayError::Config { |
| 685 | message: format!("failed to write new text file {}: {e}", new_path.display()), |
| 686 | }); |
| 687 | } |
| 688 | |
| 689 | if let Err(e) = std::fs::rename(&new_path, path) { |
| 690 | std::fs::remove_file(&new_path).ok(); |
| 691 | let hint = if let Some(b) = backup { |
| 692 | format!( |
| 693 | "\n Backup is at: {}\n \ |
| 694 | The original file was NOT modified.", |
| 695 | b.display() |
| 696 | ) |
| 697 | } else { |
| 698 | "\n The original file was NOT modified.".to_string() |
| 699 | }; |
| 700 | return Err(TraceDecayError::Config { |
| 701 | message: format!( |
| 702 | "failed to rename {} → {}: {e}{hint}", |
| 703 | new_path.display(), |
| 704 | path.display() |
| 705 | ), |
| 706 | }); |
| 707 | } |
| 708 | |
| 709 | Ok(()) |
| 710 | } |
| 711 | |
| 712 | /// Write a JSON value to a file with pretty formatting. |
| 713 | /// Creates a backup, writes atomically, and restores on failure. |
no test coverage detected