Files
clash-verge-rev/src-tauri/src/config/snapshot.rs
T
Tunglies 8328e34208 refactor(backend): declarative effects (#8128)
* refactor(core): type notice statuses behind the frozen wire contract

* refactor(utils): unified retry and strategy-list helpers

* refactor(feat): declarative effect registry for verge and clash patches

* refactor(core): derive refresh announcements from transaction commit

* refactor(core): fold tray, hotkey, autostart updates into effect ensures

* refactor(utils): migrate remaining bounded retry loops

* docs: add src-tauri architecture guardrails

* refactor(core): coalesce burst refresh announcements

* refactor(core): complete bounded service retry migration

* ci(backend): add architecture ban gates

* test(feat): freeze representative effect registry mappings

* docs(backend): record dead wire contract entries

* chore(core): document watcher exit conditions

* fix(backend): preserve applied state across later failures

* fix(config): retain runtime after an applied core replacement

* refactor(backend): flatten refresh publication and retry policies

* docs(backend): focus guardrails on architecture ownership

* refactor(effects): use one execution order

* fix(config): publish retained runtime and reconcile proxy settings
2026-10-07 07:38:55 +00:00

92 lines
3.0 KiB
Rust

use crate::utils::dirs;
use anyhow::{Context as _, Result, bail};
use std::{io::ErrorKind, path::PathBuf};
pub(crate) struct FileSnapshot {
pub(crate) path: PathBuf,
content: Option<Vec<u8>>,
}
async fn capture_files(paths: &[PathBuf]) -> Result<Vec<FileSnapshot>> {
let mut snapshots = Vec::with_capacity(paths.len());
for path in paths {
let content = match tokio::fs::read(path).await {
Ok(content) => Some(content),
Err(error) if error.kind() == ErrorKind::NotFound => None,
Err(error) => {
return Err(error).with_context(|| format!("failed to snapshot {}", path.display()));
}
};
snapshots.push(FileSnapshot {
path: path.clone(),
content,
});
}
Ok(snapshots)
}
pub(crate) async fn capture_config_files() -> Result<Vec<FileSnapshot>> {
let runtime_path = dirs::app_home_dir()?.join(crate::constants::files::RUNTIME_CONFIG);
capture_files(&[dirs::clash_path()?, dirs::verge_path()?, runtime_path]).await
}
pub(crate) async fn restore_files(snapshots: &[FileSnapshot]) -> Result<()> {
let mut errors = Vec::new();
for snapshot in snapshots {
let result = match &snapshot.content {
Some(content) => tokio::fs::write(&snapshot.path, content).await,
None => match tokio::fs::remove_file(&snapshot.path).await {
Ok(()) => Ok(()),
Err(error) if error.kind() == ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
},
};
if let Err(error) = result {
errors.push(format!("{}: {}", snapshot.path.display(), error));
}
}
if errors.is_empty() {
Ok(())
} else {
bail!("{}", errors.join("; "))
}
}
#[cfg(test)]
mod tests {
use super::{capture_files, restore_files};
#[tokio::test]
async fn restores_every_configuration_layer_after_partial_write() -> anyhow::Result<()> {
let root = std::env::temp_dir().join(format!(
"config-rollback-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_nanos()
));
tokio::fs::create_dir_all(&root).await?;
let paths = [
root.join("config.yaml"),
root.join("verge.yaml"),
root.join("clash-verge.yaml"),
];
for (index, path) in paths.iter().enumerate() {
tokio::fs::write(path, format!("original-{index}")).await?;
}
let snapshots = capture_files(&paths).await?;
tokio::fs::write(&paths[0], "changed").await?;
tokio::fs::write(&paths[1], "changed").await?;
tokio::fs::remove_file(&paths[2]).await?;
restore_files(&snapshots).await?;
for (index, path) in paths.iter().enumerate() {
assert_eq!(tokio::fs::read_to_string(path).await?, format!("original-{index}"));
}
tokio::fs::remove_dir_all(root).await?;
Ok(())
}
}