MCPcopy Create free account
hub / github.com/ScriptedAlchemy/tracedecay / safe_write_json_file

Function safe_write_json_file

src/agents/mod.rs:591–657  ·  view source on GitHub ↗

Write a JSON value to a file via atomic rename. The caller is responsible for creating the backup via [`backup_config_file`] before loading the config. Pass the backup path here so that it can be mentioned in error messages and used for restore if the rename somehow leaves the target in a bad state. # Strategy 1. Serialize → validate → write to a **new** sibling file (`.new`). The original file

(
    path: &Path,
    value: &serde_json::Value,
    backup: Option<&Path>,
)

Source from the content-addressed store, hash-verified

589///
590/// In every error case the original file remains intact.
591pub fn safe_write_json_file(
592 path: &Path,
593 value: &serde_json::Value,
594 backup: Option<&Path>,
595) -> Result<()> {
596 // 1. Serialize
597 let pretty = serde_json::to_string_pretty(value).map_err(|e| TraceDecayError::Config {
598 message: format!("failed to serialize JSON for {}: {e}", path.display()),
599 })?;
600
601 // 2. Re-parse to verify the serialized output is valid JSON
602 if serde_json::from_str::<serde_json::Value>(&pretty).is_err() {
603 return Err(TraceDecayError::Config {
604 message: format!(
605 "internal error: serialized JSON for {} failed re-parse validation.\n \
606 This is a bug in tracedecay — please report it.",
607 path.display()
608 ),
609 });
610 }
611
612 // 3. Ensure parent dir
613 if let Some(parent) = path.parent() {
614 std::fs::create_dir_all(parent).map_err(|e| TraceDecayError::Config {
615 message: format!("cannot create directory {}: {e}", parent.display()),
616 })?;
617 }
618
619 // 4. Write to a NEW sibling file — the original is never opened for
620 // writing, so an interrupted write or crash only affects the .new file.
621 let content = format!("{pretty}\n");
622 let new_path = PathBuf::from(format!("{}.new", path.display()));
623 if let Err(e) = std::fs::write(&new_path, &content) {
624 std::fs::remove_file(&new_path).ok(); // clean up partial write
625 return Err(TraceDecayError::Config {
626 message: format!(
627 "failed to write new config file {}: {e}",
628 new_path.display()
629 ),
630 });
631 }
632
633 // 5. Atomic rename: new → original.
634 // On POSIX, rename(2) atomically replaces the target.
635 // If this fails the original file is still intact.
636 if let Err(e) = std::fs::rename(&new_path, path) {
637 std::fs::remove_file(&new_path).ok(); // clean up
638 let hint = if let Some(b) = backup {
639 format!(
640 "\n Backup is at: {}\n \
641 The original file was NOT modified.",
642 b.display()
643 )
644 } else {
645 "\n The original file was NOT modified.".to_string()
646 };
647 return Err(TraceDecayError::Config {
648 message: format!(

Calls 1

writeFunction · 0.85