mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-26 22:22:13 +08:00
* refactor(ipc): generate bindings with Specta * fix(profile): project IPC inputs to consumed fields * fix(events): remove unused refresh payloads * fix(webdav): return only consumed backup metadata * fix(network): trim unused interface metadata * fix(sysinfo): return only displayed system fields * chore(knip): check generated runtime exports * fix(proxy): confirm runtime chain updates before UI changes * fix(clipboard): propagate proxy environment copy failures * refactor(dns): remove ignored runtime patch * refactor(core): trim unused upgrade response fields * refactor(ipc): avoid redundant response copies * refactor(proxy): clarify automatic proxy query naming * chore(tests): remove added IPC regression tests * refactor(ipc): enforce typed command boundaries * chore(contract): check generated bindings before push * refactor(ipc): remove unused proxy liveness payloads
372 lines
12 KiB
Rust
372 lines
12 KiB
Rust
use crate::{
|
|
config::{Config, IClashTemp, IProfiles, IVerge},
|
|
core::{CoreManager, backup, proxy_control, proxy_control::SystemProxyStateUnknown},
|
|
process::AsyncHandler,
|
|
utils::{
|
|
dirs::{PathBufExec as _, app_home_dir, local_backup_dir, verge_path},
|
|
help,
|
|
},
|
|
};
|
|
use anyhow::{Result, anyhow};
|
|
use chrono::Utc;
|
|
use clash_verge_logging::{Type, logging, logging_error};
|
|
use reqwest_dav::list_cmd::ListFile;
|
|
use serde::Serialize;
|
|
use smartstring::alias::String;
|
|
use std::path::PathBuf;
|
|
use tokio::fs;
|
|
|
|
#[derive(Debug, Serialize, specta::Type)]
|
|
pub struct LocalBackupFile {
|
|
#[specta(type = std::string::String)]
|
|
pub filename: String,
|
|
#[specta(type = std::string::String)]
|
|
pub path: String,
|
|
#[specta(type = std::string::String)]
|
|
pub last_modified: String,
|
|
pub content_length: u64,
|
|
}
|
|
|
|
/// Reloads restored configs into memory while preserving WebDAV credentials.
|
|
async fn finalize_restored_verge_config(
|
|
webdav_url: Option<String>,
|
|
webdav_username: Option<String>,
|
|
webdav_password: Option<String>,
|
|
) -> Result<()> {
|
|
// A broken restored `verge.yaml` is a restore failure, not a reason to load defaults.
|
|
let mut restored = help::read_yaml::<IVerge>(&verge_path()?).await?;
|
|
restored.webdav_url = webdav_url;
|
|
restored.webdav_username = webdav_username;
|
|
restored.webdav_password = webdav_password;
|
|
restored.save_file().await?;
|
|
|
|
let restored_clash = IClashTemp::new().await;
|
|
let clash_draft = Config::clash().await;
|
|
clash_draft.edit_draft(|d| {
|
|
*d = restored_clash.clone();
|
|
});
|
|
clash_draft.apply();
|
|
|
|
let restored_profiles = IProfiles::new().await;
|
|
let profiles_draft = Config::profiles().await;
|
|
profiles_draft.edit_draft(|d| {
|
|
*d = restored_profiles.clone();
|
|
});
|
|
profiles_draft.apply();
|
|
|
|
let verge_draft = Config::verge().await;
|
|
{
|
|
// Hold the write lock so a concurrent patch cannot stage a draft the proxy write reads.
|
|
let _config_write = Config::lock_config_write().await;
|
|
verge_draft.edit_draft(|d| {
|
|
*d = restored.clone();
|
|
});
|
|
verge_draft.apply();
|
|
|
|
// Turn it off here; a failing core side effect below would otherwise skip this step.
|
|
if !restored.enable_system_proxy.unwrap_or_default() {
|
|
let result = async {
|
|
let _lifecycle = CoreManager::global().lifecycle_lock.lock().await;
|
|
proxy_control::apply().await?;
|
|
proxy_control::refresh_guard().await
|
|
}
|
|
.await;
|
|
if let Err(err) = result {
|
|
logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}");
|
|
if SystemProxyStateUnknown::is_in(&err) {
|
|
return Err(err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run configuration side effects without rewriting the already-restored file.
|
|
if let Err(err) = super::patch_verge(&restored, true).await {
|
|
logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#}");
|
|
// Propagate unknown proxy state; ordinary side-effect failures stay logged.
|
|
if SystemProxyStateUnknown::is_in(&err) {
|
|
return Err(err);
|
|
}
|
|
}
|
|
logging_error!(Type::Config, Config::sync_dns_override().await);
|
|
Ok(())
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, level = "info")]
|
|
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
|
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
|
|
logging!(error, Type::Backup, "Failed to create backup: {err:#}");
|
|
err
|
|
})?;
|
|
|
|
if let Err(err) = backup::WebDavClient::global()
|
|
.upload(temp_file_path.clone(), file_name)
|
|
.await
|
|
{
|
|
logging!(error, Type::Backup, "Failed to upload to WebDAV: {err:#}");
|
|
backup::WebDavClient::global().reset();
|
|
return Err(err);
|
|
}
|
|
|
|
if let Err(err) = temp_file_path.remove_if_exists().await {
|
|
logging!(warn, Type::Backup, "Failed to remove temp file: {err:#}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn list_wevdav_backup() -> Result<Vec<ListFile>> {
|
|
backup::WebDavClient::global().list().await.map_err(|err| {
|
|
logging!(error, Type::Backup, "Failed to list WebDAV backup files: {err:#}");
|
|
err
|
|
})
|
|
}
|
|
|
|
pub async fn delete_webdav_backup(filename: String) -> Result<()> {
|
|
let name = filename.to_string();
|
|
backup::WebDavClient::global().delete(filename).await.map_err(|err| {
|
|
logging!(
|
|
error,
|
|
Type::Backup,
|
|
"Failed to delete WebDAV backup file {name}: {err:#}"
|
|
);
|
|
err
|
|
})
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
|
|
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
|
|
let verge = Config::verge().await;
|
|
let verge_data = verge.latest_arc();
|
|
let webdav_url = verge_data.webdav_url.clone();
|
|
let webdav_username = verge_data.webdav_username.clone();
|
|
let webdav_password = verge_data.webdav_password.clone();
|
|
|
|
let backup_storage_path = app_home_dir()
|
|
.map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))?
|
|
.join(filename.as_str());
|
|
let name = filename.to_string();
|
|
backup::WebDavClient::global()
|
|
.download(filename, backup_storage_path.clone())
|
|
.await
|
|
.map_err(|err| {
|
|
logging!(
|
|
error,
|
|
Type::Backup,
|
|
"Failed to download WebDAV backup file {name}: {err:#}"
|
|
);
|
|
err
|
|
})?;
|
|
|
|
let value = backup_storage_path.clone();
|
|
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??;
|
|
let mut zip = zip::ZipArchive::new(file)?;
|
|
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
|
zip.extract(app_home_dir()?)?;
|
|
let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await;
|
|
let _ = backup_storage_path.remove_if_exists().await;
|
|
res
|
|
}
|
|
|
|
pub async fn create_local_backup() -> Result<()> {
|
|
create_local_backup_with_namer(|name| name.to_string().into())
|
|
.await
|
|
.map(|_| ())
|
|
}
|
|
|
|
pub async fn create_local_backup_with_namer<F>(namer: F) -> Result<String>
|
|
where
|
|
F: FnOnce(&str) -> String,
|
|
{
|
|
let (file_name, temp_file_path) = backup::create_backup().await.map_err(|err| {
|
|
logging!(error, Type::Backup, "Failed to create local backup: {err:#}");
|
|
err
|
|
})?;
|
|
|
|
let backup_dir = local_backup_dir()?;
|
|
let final_name = namer(file_name.as_str());
|
|
let target_path = backup_dir.join(final_name.as_str());
|
|
|
|
if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()).await {
|
|
logging!(
|
|
error,
|
|
Type::Backup,
|
|
"Failed to move local backup file to {}: {err:#}",
|
|
target_path.display()
|
|
);
|
|
if let Err(clean_err) = temp_file_path.remove_if_exists().await {
|
|
logging!(
|
|
warn,
|
|
Type::Backup,
|
|
"Failed to remove temp backup file after move error: {clean_err:#}"
|
|
);
|
|
}
|
|
return Err(err);
|
|
}
|
|
|
|
Ok(final_name)
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, level = "info", fields(source = %source))]
|
|
pub async fn import_local_backup(source: String) -> Result<String> {
|
|
let source_path = PathBuf::from(source.as_str());
|
|
if !source_path.exists() {
|
|
return Err(anyhow!("Backup file not found: {source}"));
|
|
}
|
|
if !source_path.is_file() {
|
|
return Err(anyhow!("Backup path is not a file: {source}"));
|
|
}
|
|
|
|
let ext = source_path
|
|
.extension()
|
|
.and_then(|ext| ext.to_str())
|
|
.map(|ext| ext.to_ascii_lowercase())
|
|
.unwrap_or_default();
|
|
if ext != "zip" {
|
|
return Err(anyhow!("Only .zip backup files are supported"));
|
|
}
|
|
|
|
let file_name = source_path
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.ok_or_else(|| anyhow!("Invalid backup file name"))?;
|
|
|
|
let backup_dir = local_backup_dir()?;
|
|
let target_path = backup_dir.join(file_name);
|
|
|
|
if target_path == source_path {
|
|
return Ok(file_name.to_string().into());
|
|
}
|
|
|
|
if let Some(parent) = target_path.parent() {
|
|
fs::create_dir_all(parent).await?;
|
|
}
|
|
|
|
if target_path.exists() {
|
|
return Err(anyhow!("Backup file already exists: {file_name}"));
|
|
}
|
|
|
|
fs::copy(&source_path, &target_path)
|
|
.await
|
|
.map_err(|err| anyhow!("Failed to import backup file: {err:#}"))?;
|
|
|
|
Ok(file_name.to_string().into())
|
|
}
|
|
|
|
async fn move_file(from: PathBuf, to: PathBuf) -> Result<()> {
|
|
if let Some(parent) = to.parent() {
|
|
fs::create_dir_all(parent).await?;
|
|
}
|
|
|
|
match fs::rename(&from, &to).await {
|
|
Ok(_) => Ok(()),
|
|
Err(rename_err) => {
|
|
// Rename can fail across filesystems; fall back to copy then remove.
|
|
logging!(
|
|
warn,
|
|
Type::Backup,
|
|
"Failed to rename backup file directly, fallback to copy/remove: {rename_err}"
|
|
);
|
|
fs::copy(&from, &to)
|
|
.await
|
|
.map_err(|err| anyhow!("Failed to copy backup file: {err:#}"))?;
|
|
fs::remove_file(&from)
|
|
.await
|
|
.map_err(|err| anyhow!("Failed to remove temp backup file: {err:#}"))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn list_local_backup() -> Result<Vec<LocalBackupFile>> {
|
|
let backup_dir = local_backup_dir()?;
|
|
if !backup_dir.exists() {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
let mut backups = Vec::new();
|
|
let mut dir = fs::read_dir(&backup_dir).await?;
|
|
while let Some(entry) = dir.next_entry().await? {
|
|
let path = entry.path();
|
|
let metadata = entry.metadata().await?;
|
|
if !metadata.is_file() {
|
|
continue;
|
|
}
|
|
|
|
let file_name = match path.file_name().and_then(|name| name.to_str()) {
|
|
Some(name) => name,
|
|
None => continue,
|
|
};
|
|
let last_modified = metadata
|
|
.modified()
|
|
.map(|time| chrono::DateTime::<Utc>::from(time).to_rfc3339())
|
|
.unwrap_or_default();
|
|
backups.push(LocalBackupFile {
|
|
filename: file_name.into(),
|
|
path: path.to_string_lossy().into(),
|
|
last_modified: last_modified.into(),
|
|
content_length: metadata.len(),
|
|
});
|
|
}
|
|
|
|
backups.sort_by(|a, b| b.filename.cmp(&a.filename));
|
|
Ok(backups)
|
|
}
|
|
|
|
pub async fn delete_local_backup(filename: String) -> Result<()> {
|
|
let backup_dir = local_backup_dir()?;
|
|
let target_path = backup_dir.join(filename.as_str());
|
|
if !target_path.exists() {
|
|
logging!(debug, Type::Backup, "Local backup file not found: {}", filename);
|
|
return Ok(());
|
|
}
|
|
target_path.remove_if_exists().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename))]
|
|
pub async fn restore_local_backup(filename: String) -> Result<()> {
|
|
let backup_dir = local_backup_dir()?;
|
|
let target_path = backup_dir.join(filename.as_str());
|
|
if !target_path.exists() {
|
|
return Err(anyhow!("Backup file not found: {}", filename));
|
|
}
|
|
|
|
let (webdav_url, webdav_username, webdav_password) = {
|
|
let verge = Config::verge().await;
|
|
let verge = verge.latest_arc();
|
|
(
|
|
verge.webdav_url.clone(),
|
|
verge.webdav_username.clone(),
|
|
verge.webdav_password.clone(),
|
|
)
|
|
};
|
|
|
|
let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??;
|
|
let mut zip = zip::ZipArchive::new(file)?;
|
|
let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await;
|
|
zip.extract(app_home_dir()?)?;
|
|
finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))]
|
|
pub async fn export_local_backup(filename: String, destination: String) -> Result<()> {
|
|
let backup_dir = local_backup_dir()?;
|
|
let source_path = backup_dir.join(filename.as_str());
|
|
if !source_path.exists() {
|
|
return Err(anyhow!("Backup file not found: {}", filename));
|
|
}
|
|
|
|
let dest_path = PathBuf::from(destination.as_str());
|
|
if let Some(parent) = dest_path.parent() {
|
|
fs::create_dir_all(parent).await?;
|
|
}
|
|
|
|
fs::copy(&source_path, &dest_path)
|
|
.await
|
|
.map(|_| ())
|
|
.map_err(|err| anyhow!("Failed to export backup file: {err:#}"))?;
|
|
Ok(())
|
|
}
|