fix: make profile deletion rollback-safe

This commit is contained in:
Tunglies
2026-08-03 20:00:01 +08:00
parent 903410c12e
commit c5b641e421
14 changed files with 264 additions and 113 deletions
+4 -4
View File
@@ -3,7 +3,7 @@ use crate::feat;
use crate::utils::{dirs, yaml_emitter};
use crate::{
cmd::StringifyErr as _,
config::{ClashInfo, Config},
config::{ClashInfo, Config, profiles::profiles_save_file_safe},
constants,
core::{
CoreManager, handle,
@@ -64,7 +64,7 @@ pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>>
match CoreManager::global().change_core(&clash_core).await {
Ok(_) => {
logging_error!(Type::Core, Config::profiles().await.data_arc().save_file().await);
logging_error!(Type::Core, profiles_save_file_safe().await);
// 切换内核后重启内核
match CoreManager::global().restart_core().await {
@@ -107,7 +107,7 @@ pub async fn start_core() -> CmdResult {
/// 关闭核心
#[tauri::command]
pub async fn stop_core() -> CmdResult {
logging_error!(Type::Core, Config::profiles().await.data_arc().save_file().await);
logging_error!(Type::Core, profiles_save_file_safe().await);
let result = CoreManager::global()
.stop_core()
.await
@@ -121,7 +121,7 @@ pub async fn stop_core() -> CmdResult {
/// 重启核心
#[tauri::command]
pub async fn restart_core() -> CmdResult {
logging_error!(Type::Core, Config::profiles().await.data_arc().save_file().await);
logging_error!(Type::Core, profiles_save_file_safe().await);
let result = CoreManager::global()
.restart_core()
.await
+42 -25
View File
@@ -6,7 +6,7 @@ use crate::{
config::{
Config, IProfiles, PrfItem, PrfOption,
profiles::{
profiles_append_item_with_filedata_safe, profiles_delete_item_safe, profiles_patch_item_safe,
PROFILE_WRITE_LOCK, profiles_append_item_with_filedata_safe, profiles_patch_item_safe,
profiles_reorder_safe, profiles_save_file_safe,
},
profiles_append_item_safe,
@@ -153,13 +153,44 @@ pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResu
/// 删除配置文件
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
// 使用Send-safe helper函数
let should_update = profiles_delete_item_safe(&index)
.await
.with_error_code("PROFILE_DELETE_FAILED")?;
profiles_save_file_safe()
let profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let profiles = Config::profiles().await;
let result = profiles
.with_data_modify(|mut candidate| async move {
let original = candidate.clone();
let (should_update, plan) = candidate.plan_delete_item(&index)?;
let guard = if should_update {
match CoreManager::global()
.update_config_forced_with_profiles(&candidate, &original)
.await?
{
Ok(guard) => Some(guard),
Err(outcome) => return Ok((original, Err(outcome))),
}
} else {
candidate.save_file().await?;
None
};
let current = candidate.current.clone();
plan.cleanup().await;
Ok((candidate, Ok((should_update, current, guard))))
})
.await
.with_error_code("PROFILE_DELETE_FAILED")?;
let (should_update, current, config_update_guard) = match result {
Ok(result) => result,
Err(outcome) => {
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
return Err(coded_error("PROFILE_DELETE_FAILED", outcome));
}
};
if should_update {
logging_error!(Type::Config, profiles::activate_selected_nodes());
}
drop(config_update_guard);
drop(profile_write_guard);
if let Err(e) = Tray::global().update_tooltip().await {
logging!(warn, Type::Cmd, "Warning: 异步更新托盘提示失败: {e}");
}
@@ -168,28 +199,13 @@ pub async fn delete_profile(index: String) -> CmdResult {
logging!(warn, Type::Cmd, "Warning: 异步更新托盘菜单失败: {e}");
}
if should_update {
match CoreManager::global().update_config_forced().await {
Ok(outcome) if outcome.is_valid() => {
handle::Handle::refresh_clash();
// 发送配置变更通知
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index);
handle::Handle::notify_profile_changed(&index);
}
Ok(outcome) => {
logging!(warn, Type::Cmd, "删除订阅后更新配置失败: {}", outcome);
handle_validation_notice(&outcome, ValidationNoticeTarget::Runtime, "运行时配置");
return Err(coded_error("PROFILE_DELETE_FAILED", outcome));
}
Err(e) => {
logging!(error, Type::Cmd, "{}", e);
return Err(coded_error("PROFILE_DELETE_FAILED", e));
if let Some(current) = current.as_ref() {
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", current);
handle::Handle::notify_profile_changed(current);
}
}
}
Timer::global()
.refresh()
.await
.with_error_code("PROFILE_DELETE_FAILED")?;
logging_error!(Type::Timer, Timer::global().refresh().await);
Ok(())
}
@@ -316,6 +332,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult<ValidationO
logging!(info, Type::Cmd, "当前正在切换配置,放弃请求");
return Ok(ValidationOutcome::Busy);
}
let _profile_write_guard = PROFILE_WRITE_LOCK.lock().await;
let target_profile = profiles.current.as_ref();
+7 -1
View File
@@ -244,7 +244,12 @@ impl Config {
}
pub async fn generate() -> Result<()> {
let (mut config, exists_keys, logs) = enhance::enhance().await?;
let profiles = Self::profiles().await.latest_arc();
Self::generate_with_profiles(&profiles).await
}
pub(crate) async fn generate_with_profiles(profiles: &IProfiles) -> Result<()> {
let (mut config, exists_keys, logs) = enhance::enhance(profiles).await?;
sanitize_tunnels_proxy(&mut config);
@@ -300,6 +305,7 @@ impl Config {
});
let save_profiles_task = AsyncHandler::spawn(|| async {
let _profile_write = super::profiles::PROFILE_WRITE_LOCK.lock().await;
let profiles = Self::profiles().await;
profiles.apply();
logging_error!(Type::Config, profiles.data_arc().save_file().await);
+63 -31
View File
@@ -28,6 +28,8 @@ use std::{
use tauri_plugin_mihomo::models::{Proxies, ProxyType};
use tokio::{fs, task::JoinHandle};
pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
/// Regex to check profile file names, eg.
/// R12345678.yaml (remote)
/// L12345678.yaml (local)
@@ -88,6 +90,23 @@ pub struct CleanupResult {
pub failed_deletions: usize,
}
pub(crate) struct ProfileDeletePlan {
files: Vec<String>,
}
impl ProfileDeletePlan {
pub(crate) async fn cleanup(self) {
let Ok(dir) = dirs::app_profiles_dir() else {
return;
};
for file in self.files {
if let Err(error) = dir.join(file.as_str()).remove_if_exists().await {
logging!(warn, Type::Config, "清理已删除订阅文件失败: {file} - {error}");
}
}
}
}
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
@@ -130,7 +149,7 @@ impl IProfiles {
}
pub async fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await
help::save_yaml_atomic(&dirs::profiles_path()?, self, Some("# Profiles Config for Clash Verge")).await
}
/// 只修改current,valid和chain
@@ -331,15 +350,9 @@ impl IProfiles {
self.save_file().await
}
/// delete item
/// if delete the current then return true
pub async fn delete_item(&mut self, uid: &String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(uid);
let current = current.clone();
let delete_uids = {
let item = self.get_item(uid)?;
let option = item.option.as_ref();
option.map_or(Vec::new(), |op| {
pub(crate) fn plan_delete_item(&mut self, uid: &String) -> Result<(bool, ProfileDeletePlan)> {
let current = self.current.as_ref().unwrap_or(uid).clone();
let delete_uids = self.get_item(uid)?.option.as_ref().map_or_else(Vec::new, |op| {
[
op.merge.clone(),
op.script.clone(),
@@ -349,22 +362,20 @@ impl IProfiles {
]
.into_iter()
.collect::<Vec<_>>()
})
};
});
let mut items = self.items.take().unwrap_or_default();
let mut files = Vec::new();
// remove the main item (if exists) and delete its file
if let Some(file) = Self::take_item_file_by_uid(&mut items, Some(uid.as_str())) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
files.push(file);
}
for delete_uid in delete_uids {
if let Some(file) = Self::take_item_file_by_uid(&mut items, delete_uid.as_deref()) {
let _ = dirs::app_profiles_dir()?.join(file.as_str()).remove_if_exists().await;
files.push(file);
}
}
// delete the original uid
if current == *uid {
self.current = None;
for item in items.iter() {
@@ -376,8 +387,7 @@ impl IProfiles {
}
self.items = Some(items);
self.save_file().await?;
Ok(current == *uid)
Ok((current == *uid, ProfileDeletePlan { files }))
}
/// 获取current指向的订阅内容
@@ -626,16 +636,6 @@ pub async fn profiles_patch_item_safe(index: &String, item: &PrfItem) -> Result<
.await
}
pub async fn profiles_delete_item_safe(index: &String) -> Result<bool> {
Config::profiles()
.await
.with_data_modify(|mut profiles| async move {
let deleted = profiles.delete_item(index).await?;
Ok((profiles, deleted))
})
.await
}
pub async fn profiles_reorder_safe(active_id: &String, over_id: &String) -> Result<()> {
Config::profiles()
.await
@@ -795,9 +795,8 @@ fn reconcile_selected_nodes(
/// already holding the newer one, would then disagree with the core. Bumping the generation is
/// how an activation is told it has been overtaken.
///
/// Called *after* the profile is written, which is the only placement that needs to exist: an
/// activation that read the older profile is cancelled by this, and one starting afterwards reads
/// the newer one and needs no cancelling.
/// Called after a newer selection is written, or before a candidate Core transition whose caller
/// will reactivate whichever Profiles snapshot ultimately commits.
pub fn supersede_selected_activation() {
ACTIVATE_SELECTED_GENERATION.fetch_add(1, Ordering::AcqRel);
}
@@ -1277,6 +1276,39 @@ mod tests {
use super::*;
use tauri_plugin_mihomo::models::Proxy;
fn deletion_item(uid: &str, kind: &str, file: &str, merge: Option<&str>) -> PrfItem {
PrfItem {
uid: Some(uid.into()),
itype: Some(kind.into()),
file: Some(file.into()),
option: merge.map(|uid| PrfOption {
merge: Some(uid.into()),
..PrfOption::default()
}),
..PrfItem::default()
}
}
#[test]
fn delete_plan_defers_files_and_selects_replacement() -> Result<()> {
let mut profiles = IProfiles {
current: Some("a".into()),
items: Some(vec![
deletion_item("a", "remote", "a.yaml", Some("owned")),
deletion_item("owned", "merge", "owned.yaml", None),
deletion_item("b", "local", "b.yaml", None),
]),
};
let (should_update, plan) = profiles.plan_delete_item(&"a".into())?;
assert!(should_update);
assert_eq!(profiles.current.as_deref(), Some("b"));
assert_eq!(plan.files, vec![String::from("a.yaml"), String::from("owned.yaml")]);
assert!(profiles.get_item("owned").is_err());
Ok(())
}
fn selected(group: &str, node: &str) -> PrfSelected {
PrfSelected {
name: Some(group.into()),
+68 -6
View File
@@ -1,7 +1,7 @@
use super::{CoreManager, RunningMode};
use super::{CoreManager, PROFILE_SELECTIONS_PENDING_COMMIT, RunningMode};
use crate::core::service::StageRequest;
use crate::{
config::{Config, ConfigType, runtime::IRuntime},
config::{Config, ConfigType, IProfiles, runtime::IRuntime},
constants::timing,
core::{
handle,
@@ -55,6 +55,14 @@ enum ConfigApplication {
Fail(String),
}
pub(crate) struct ConfigUpdateGuard<'a>(&'a CoreManager);
impl Drop for ConfigUpdateGuard<'_> {
fn drop(&mut self) {
self.0.finish_config_update();
}
}
/// Decide how to apply a configuration, given how staging went.
///
/// Two branches avoid replacing the Core, for opposite reasons. One is success. The other is a
@@ -208,7 +216,57 @@ impl CoreManager {
self.set_last_update(Instant::now());
}
self.perform_config_update().await
self.perform_config_update(None).await
}
pub(crate) async fn update_config_forced_with_profiles(
&self,
candidate: &IProfiles,
rollback: &IProfiles,
) -> Result<std::result::Result<ConfigUpdateGuard<'_>, ValidationOutcome>> {
if handle::Handle::global().is_exiting() {
return Ok(Err(ValidationOutcome::Skipped {
reason: ValidationSkipReason::Exiting,
}));
}
if !self.try_start_config_update() {
return Ok(Err(ValidationOutcome::Busy));
}
let guard = ConfigUpdateGuard(self);
self.set_last_update(Instant::now());
crate::config::profiles::supersede_selected_activation();
let outcome = match PROFILE_SELECTIONS_PENDING_COMMIT
.scope(true, self.perform_config_update(Some(candidate)))
.await
{
Ok(outcome) => outcome,
Err(error) => {
self.restore_profile_config(rollback).await?;
return Err(error);
}
};
if !outcome.is_valid() {
crate::config::profiles::restore_selected_nodes().await;
return Ok(Err(outcome));
}
match candidate.save_file().await {
Ok(()) => Ok(Ok(guard)),
Err(error) => {
self.restore_profile_config(rollback).await?;
Err(error)
}
}
}
async fn restore_profile_config(&self, profiles: &IProfiles) -> Result<()> {
let outcome = self.perform_config_update(Some(profiles)).await?;
if outcome.is_valid() {
crate::config::profiles::restore_selected_nodes().await;
Ok(())
} else {
Err(anyhow!("failed to restore previous Core configuration: {outcome}"))
}
}
pub async fn update_config_checked(&self) -> Result<()> {
@@ -234,11 +292,15 @@ impl CoreManager {
true
}
async fn perform_config_update(&self) -> Result<ValidationOutcome> {
async fn perform_config_update(&self, profiles: Option<&IProfiles>) -> Result<ValidationOutcome> {
let runtime = Config::runtime().await;
let transaction = DraftTransaction::begin(vec![&runtime])?;
if let Err(err) = Config::generate().await {
let generate = match profiles {
Some(profiles) => Config::generate_with_profiles(profiles).await,
None => Config::generate().await,
};
if let Err(err) = generate {
let message: String = err.to_string().into();
return Ok(ValidationOutcome::invalid_from_message(message));
}
@@ -360,7 +422,7 @@ impl CoreManager {
Type::Core,
"Failed to apply configuration by mihomo api, restart core to apply it, error msg: {err}"
);
match self.restart_core().await {
match self.restart_core_during_config_update().await {
Ok(_) => {
logging!(info, Type::Core, "Configuration applied after restart");
Ok(())
+20
View File
@@ -266,6 +266,16 @@ impl CoreManager {
}
pub async fn start_core(&self) -> Result<()> {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
self.finish_config_update();
}
self.start_core_during_config_update().await
}
pub(crate) async fn start_core_during_config_update(&self) -> Result<()> {
let _life = self.lifecycle_lock.lock().await;
run_core_start_transition(
|| self.start_core_inner(),
@@ -498,6 +508,16 @@ impl CoreManager {
}
pub async fn restart_core(&self) -> Result<()> {
if !self.try_start_config_update() {
anyhow::bail!("configuration update is already running");
}
defer! {
self.finish_config_update();
}
self.restart_core_during_config_update().await
}
pub(crate) async fn restart_core_during_config_update(&self) -> Result<()> {
// 持锁覆盖 stop+start,避免生命周期操作插入。
let _life = self.lifecycle_lock.lock().await;
logging!(info, Type::Core, "Restarting core");
+4
View File
@@ -24,6 +24,10 @@ use std::os::windows::io::OwnedHandle;
pub(crate) static CLASH_LOGGER: Lazy<Arc<AsyncLogger>> = Lazy::new(|| Arc::new(AsyncLogger::new()));
tokio::task_local! {
static PROFILE_SELECTIONS_PENDING_COMMIT: bool;
}
const CORE_READINESS_ACTIVE_BIT: u64 = 1;
const CORE_READINESS_GENERATION_STEP: u64 = 1 << 1;
+7 -1
View File
@@ -1,6 +1,6 @@
#[cfg(test)]
use super::claim_core_readiness_generation;
use super::{CoreManager, RunningMode};
use super::{CoreManager, PROFILE_SELECTIONS_PENDING_COMMIT, RunningMode};
use crate::{
AsyncHandler,
config::Config,
@@ -37,6 +37,12 @@ impl CoreManager {
///
/// Repeat calls supersede each other, so every start path may call this without coordinating.
async fn restore_selected_nodes(&self) {
if PROFILE_SELECTIONS_PENDING_COMMIT
.try_with(|pending| *pending)
.unwrap_or(false)
{
return;
}
crate::config::profiles::restore_selected_nodes().await;
}
}
+15 -24
View File
@@ -15,7 +15,7 @@ use self::{
};
use crate::utils::dirs;
use crate::{
config::{Config, IVerge, PrfItem},
config::{Config, IProfiles, IVerge, PrfItem},
constants,
utils::tmpl,
};
@@ -157,25 +157,18 @@ async fn get_config_values() -> ConfigValues {
}
#[allow(clippy::cognitive_complexity)]
async fn collect_profile_items() -> Result<ProfileItems> {
let profiles = Config::profiles().await;
let profiles_arc = profiles.latest_arc();
drop(profiles);
let current_profile_uid = match profiles_arc.get_current().cloned() {
async fn collect_profile_items(profiles: &IProfiles) -> Result<ProfileItems> {
let current_profile_uid = match profiles.get_current().cloned() {
Some(uid) => uid,
None => {
drop(profiles_arc);
return Ok(ProfileItems::default());
}
None => return Ok(ProfileItems::default()),
};
let current = profiles_arc
let current = profiles
.current_mapping()
.await
.with_context(|| format!("failed to read current profile \"{current_profile_uid}\""))?;
let current_item = match profiles_arc.get_item(&current_profile_uid) {
let current_item = match profiles.get_item(&current_profile_uid) {
Ok(item) => item,
Err(err) => {
return Err(err).with_context(|| format!("failed to get current profile \"{current_profile_uid}\""));
@@ -200,38 +193,36 @@ async fn collect_profile_items() -> Result<ProfileItems> {
let name = current_item.name.clone().unwrap_or_default();
let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!(
chain_item_or_default(profiles_arc.get_item(&merge_uid).ok(), || ChainItem {
chain_item_or_default(profiles.get_item(&merge_uid).ok(), || ChainItem {
uid: "".into(),
data: ChainType::Merge(Mapping::new()),
},),
chain_item_or_default(profiles_arc.get_item(&script_uid).ok(), || ChainItem {
chain_item_or_default(profiles.get_item(&script_uid).ok(), || ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
},),
chain_item_or_default(profiles_arc.get_item(&rules_uid).ok(), || ChainItem {
chain_item_or_default(profiles.get_item(&rules_uid).ok(), || ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
},),
chain_item_or_default(profiles_arc.get_item(&proxies_uid).ok(), || ChainItem {
chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
},),
chain_item_or_default(profiles_arc.get_item(&groups_uid).ok(), || ChainItem {
chain_item_or_default(profiles.get_item(&groups_uid).ok(), || ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
},),
chain_item_or_default(profiles_arc.get_item("Merge").ok(), || ChainItem {
chain_item_or_default(profiles.get_item("Merge").ok(), || ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
},),
chain_item_or_default(profiles_arc.get_item("Script").ok(), || ChainItem {
chain_item_or_default(profiles.get_item("Script").ok(), || ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
},),
);
drop(profiles_arc);
Ok(ProfileItems {
config: current,
merge_item,
@@ -713,7 +704,7 @@ async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> M
/// Enhance mode
/// 返回最终订阅、该订阅包含的键、和script执行的结果
pub async fn enhance() -> Result<(Mapping, HashSet<String>, HashMap<String, ResultLog>)> {
pub async fn enhance(profiles: &IProfiles) -> Result<(Mapping, HashSet<String>, HashMap<String, ResultLog>)> {
// gather config values
let cfg_vals = get_config_values().await;
let ConfigValues {
@@ -732,7 +723,7 @@ pub async fn enhance() -> Result<(Mapping, HashSet<String>, HashMap<String, Resu
} = cfg_vals;
// collect profile items
let profile = collect_profile_items().await?;
let profile = collect_profile_items(profiles).await?;
let config = profile.config;
let merge_item = profile.merge_item;
let script_item = profile.script_item;
+2
View File
@@ -130,6 +130,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
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;
// Finally remove the temp file (attempt cleanup even if finalize fails)
@@ -313,6 +314,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> {
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(())
+3 -3
View File
@@ -91,7 +91,7 @@ pub async fn save_proxy_ports(settings: ProxyPortSettings) -> Result<SaveProxyPo
};
}
if was_running && let Err(activation_error) = manager.restart_core().await {
if was_running && let Err(activation_error) = manager.restart_core_during_config_update().await {
transaction.rollback();
if let Err(rollback_error) = rollback_proxy_ports(&snapshots, was_running).await {
return Err(anyhow!(
@@ -233,9 +233,9 @@ async fn rollback_proxy_ports(snapshots: &[FileSnapshot], was_running: bool) ->
let lifecycle_result = if was_running {
let manager = CoreManager::global();
if matches!(*manager.get_running_mode(), RunningMode::NotRunning) {
manager.start_core().await
manager.start_core_during_config_update().await
} else {
manager.restart_core().await
manager.restart_core_during_config_update().await
}
} else {
Ok(())
+14 -3
View File
@@ -4,9 +4,10 @@ use clash_verge_logging::{Type, logging};
use nanoid::nanoid;
use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::{Mapping, Value};
#[cfg(target_os = "windows")]
use std::path::Path;
use std::{path::PathBuf, str::FromStr};
use std::{
path::{Path, PathBuf},
str::FromStr,
};
/// read data from yaml as struct T
pub async fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
@@ -73,6 +74,16 @@ pub async fn save_yaml<T: Serialize + Sync>(path: &PathBuf, data: &T, prefix: Op
Ok(())
}
pub async fn save_yaml_atomic<T: Serialize + Sync>(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> {
let temporary = path.with_extension("delete.tmp");
save_yaml(&temporary, data, prefix).await?;
if let Err(error) = super::server::replace_file_atomic(&temporary, path) {
let _ = tokio::fs::remove_file(&temporary).await;
return Err(error).with_context(|| format!("failed to replace file \"{}\"", path.display()));
}
Ok(())
}
const ALPHABET: [char; 62] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+1 -1
View File
@@ -97,7 +97,7 @@ async fn import_subscription(url: &str, name: Option<&String>) {
return;
}
if let Err(e) = Config::profiles().await.data_arc().save_file().await {
if let Err(e) = profiles::profiles_save_file_safe().await {
logging!(error, Type::Config, "failed to save imported subscription: {}", e);
handle::Handle::notice_message("import_sub_url::error", e.to_string());
return;
+4 -4
View File
@@ -414,18 +414,18 @@ fn write_instance_record(path: &Path, record: &InstanceRecord) -> Result<()> {
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
}
drop(file);
replace_file_atomic(&temporary, path)?;
replace_file_atomic(&temporary, path).context("failed to replace singleton record")?;
Ok(())
}
#[cfg(not(windows))]
fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> {
pub(crate) fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> {
std::fs::rename(source, destination)?;
Ok(())
}
#[cfg(windows)]
fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> {
pub(crate) fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> {
use std::os::windows::ffi::OsStrExt as _;
use windows_sys::Win32::Storage::FileSystem::{MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW};
@@ -441,7 +441,7 @@ fn replace_file_atomic(source: &Path, destination: &Path) -> Result<()> {
)
} == 0
{
return Err(std::io::Error::last_os_error()).context("failed to replace singleton record");
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}