git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit060a42b23926b0a59fb589b93e9dadd4e845c263
parentf665688c6e
authorLucas Galante <[email protected]>
date2026-06-28 05:21
Implement atomic config writes and rolling configuration backups

 src/config_manager.rs  | 36 +++++++++++++++++++++++++++++++++++-
 src/pages/interface.rs | 49 ++++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 77 insertions(+), 8 deletions(-)

diff --git a/src/config_manager.rs b/src/config_manager.rs
index d35d5ef..5b5fda7 100644
--- a/src/config_manager.rs
+++ b/src/config_manager.rs
@@ -7,8 +7,42 @@ pub fn read_config_file() -> String {
     fs::read_to_string(CONFIG_PATH).unwrap_or_default()
 }
 
+fn perform_rolling_backup(path: &str) {
+    if path != CONFIG_PATH {
+        return;
+    }
+    if !std::path::Path::new(path).exists() {
+        return;
+    }
+    let backup_dir = "/home/lsgalante/.config/cce/backups";
+    if let Err(_) = fs::create_dir_all(backup_dir) {
+        return;
+    }
+    for i in (1..=4).rev() {
+        let src = format!("{}/config.json.{}.bak", backup_dir, i);
+        let dst = format!("{}/config.json.{}.bak", backup_dir, i + 1);
+        if std::path::Path::new(&src).exists() {
+            let _ = fs::rename(src, dst);
+        }
+    }
+    let dst = format!("{}/config.json.1.bak", backup_dir);
+    let _ = fs::copy(path, dst);
+}
+
+fn safe_write(path: &str, content: &str) -> bool {
+    perform_rolling_backup(path);
+    let temp_path = format!("{}.tmp", path);
+    if fs::write(&temp_path, content).is_ok() {
+        if fs::rename(&temp_path, path).is_ok() {
+            return true;
+        }
+        let _ = fs::remove_file(&temp_path);
+    }
+    false
+}
+
 pub fn write_config_file(content: &str) -> bool {
-    fs::write(CONFIG_PATH, content).is_ok()
+    safe_write(CONFIG_PATH, content)
 }
 
 pub fn update_json_value<F>(update_fn: F) -> bool 
diff --git a/src/pages/interface.rs b/src/pages/interface.rs
index fe51c11..edfe803 100644
--- a/src/pages/interface.rs
+++ b/src/pages/interface.rs
@@ -48,6 +48,40 @@ fn get_config_path() -> String {
 }
 
 
+fn perform_rolling_backup(path: &str) {
+    if path != CONFIG_PATH {
+        return;
+    }
+    if !std::path::Path::new(path).exists() {
+        return;
+    }
+    let backup_dir = "/home/lsgalante/.config/cce/backups";
+    if let Err(_) = fs::create_dir_all(backup_dir) {
+        return;
+    }
+    for i in (1..=4).rev() {
+        let src = format!("{}/config.json.{}.bak", backup_dir, i);
+        let dst = format!("{}/config.json.{}.bak", backup_dir, i + 1);
+        if std::path::Path::new(&src).exists() {
+            let _ = fs::rename(src, dst);
+        }
+    }
+    let dst = format!("{}/config.json.1.bak", backup_dir);
+    let _ = fs::copy(path, dst);
+}
+
+fn safe_write(path: &str, content: &str) -> bool {
+    perform_rolling_backup(path);
+    let temp_path = format!("{}.tmp", path);
+    if fs::write(&temp_path, content).is_ok() {
+        if fs::rename(&temp_path, path).is_ok() {
+            return true;
+        }
+        let _ = fs::remove_file(&temp_path);
+    }
+    false
+}
+
 fn get_socket_path() -> String {
     match std::env::var("WAYLAND_DISPLAY") {
         Ok(display) => format!("/tmp/cce-{}.sock", display),
@@ -1214,7 +1248,7 @@ pub fn write_config_value_path(path: &str, key: &str, value: &str) -> bool {
 
     if updated_any {
         if let Ok(updated_str) = serde_json::to_string_pretty(&val) {
-            if fs::write(path, updated_str).is_ok() {
+            if safe_write(path, &updated_str) {
                 return true;
             }
         }
@@ -3843,7 +3877,8 @@ pub fn parse_transparency_opacity(content: &str) -> f32 {
 }
 
 pub fn write_transparency_config_value(key: &str, value: &str) {
-    let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+    let path = get_config_path();
+    let content = fs::read_to_string(&path).unwrap_or_default();
     let mut val = parse_json(&content);
     let j_val = if let Ok(parsed_val) = serde_json::from_str::<serde_json::Value>(value) {
         parsed_val
@@ -3859,12 +3894,12 @@ pub fn write_transparency_config_value(key: &str, value: &str) {
         transparency.insert(key.to_string(), j_val);
     }
     if let Ok(updated_str) = serde_json::to_string_pretty(&val) {
-        let _ = fs::write(CONFIG_PATH, updated_str);
+        let _ = safe_write(&path, &updated_str);
     }
 }
 
 fn write_surfaces_config_value(key: &str, value: &str) {
-    write_surfaces_config_value_path(CONFIG_PATH, key, value);
+    write_surfaces_config_value_path(&get_config_path(), key, value);
 }
 
 fn write_surfaces_config_value_path(path: &str, key: &str, value: &str) {
@@ -3884,7 +3919,7 @@ fn write_surfaces_config_value_path(path: &str, key: &str, value: &str) {
         surfaces.insert(key.to_string(), j_val);
     }
     if let Ok(updated_str) = serde_json::to_string_pretty(&val) {
-        let _ = fs::write(path, updated_str);
+        let _ = safe_write(path, &updated_str);
     }
 }
 
@@ -3928,7 +3963,7 @@ fn parse_notifications_opacity(content: &str) -> f32 {
 }
 
 fn write_notifications_config_value(key: &str, value: &str) {
-    write_notifications_config_value_path(CONFIG_PATH, key, value);
+    write_notifications_config_value_path(&get_config_path(), key, value);
 }
 
 fn write_notifications_config_value_path(path: &str, key: &str, value: &str) {
@@ -3948,7 +3983,7 @@ fn write_notifications_config_value_path(path: &str, key: &str, value: &str) {
         notifications.insert(key.to_string(), j_val);
     }
     if let Ok(updated_str) = serde_json::to_string_pretty(&val) {
-        let _ = fs::write(path, updated_str);
+        let _ = safe_write(path, &updated_str);
     }
 }