fix(profile): preserve files until explicit deletion

This commit is contained in:
Tunglies
2026-08-30 14:34:28 +08:00
parent 632c5b2bc5
commit 44f6f6e19b
2 changed files with 2 additions and 182 deletions
+2 -35
View File
@@ -132,20 +132,11 @@ impl Config {
Self::runtime().await.apply(); Self::runtime().await.apply();
{
let profiles = Self::profiles().await.data_arc();
let _ = profiles.cleanup_orphaned_files().await;
}
Ok(()) Ok(())
} }
async fn ensure_default_profile_items() -> Result<()> { async fn ensure_default_profile_items() -> Result<()> {
let profiles = Self::profiles().await; let profiles = Self::profiles().await;
Self::ensure_default_profile_items_for(&profiles).await
}
async fn ensure_default_profile_items_for(profiles: &Draft<IProfiles>) -> Result<()> {
if profiles.latest_arc().items.is_none() { if profiles.latest_arc().items.is_none() {
logging!( logging!(
warn, warn,
@@ -157,11 +148,11 @@ impl Config {
if profiles.latest_arc().get_item("Merge").is_err() { if profiles.latest_arc().get_item("Merge").is_err() {
let merge_item = &mut PrfItem::from_merge(Some("Merge".into())); let merge_item = &mut PrfItem::from_merge(Some("Merge".into()));
profiles_append_item_to_safe(profiles, merge_item).await?; profiles_append_item_to_safe(&profiles, merge_item).await?;
} }
if profiles.latest_arc().get_item("Script").is_err() { if profiles.latest_arc().get_item("Script").is_err() {
let script_item = &mut PrfItem::from_script(Some("Script".into())); let script_item = &mut PrfItem::from_script(Some("Script".into()));
profiles_append_item_to_safe(profiles, script_item).await?; profiles_append_item_to_safe(&profiles, script_item).await?;
} }
Ok(()) Ok(())
} }
@@ -380,27 +371,3 @@ pub(crate) enum ConfigType {
Run, Run,
Check, Check,
} }
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn failed_profile_index_survives_startup_without_cleanup() -> Result<()> {
let profiles = Draft::new(IProfiles::default());
let profiles_dir = std::env::temp_dir().join(format!("clash-verge-profile-cleanup-{}", nanoid::nanoid!()));
tokio::fs::create_dir_all(&profiles_dir).await?;
let active_profile = profiles_dir.join("Ractive.yaml");
tokio::fs::write(&active_profile, "proxies: []").await?;
Config::ensure_default_profile_items_for(&profiles).await?;
profiles.data_arc().cleanup_orphaned_files_in(&profiles_dir).await?;
let profile_was_preserved = tokio::fs::try_exists(&active_profile).await?;
tokio::fs::remove_dir_all(&profiles_dir).await?;
assert!(profile_was_preserved);
assert!(profiles.data_arc().items.is_none());
Ok(())
}
}
-147
View File
@@ -30,11 +30,6 @@ use tokio::{fs, task::JoinHandle};
pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); pub(crate) static PROFILE_WRITE_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
/// Matches generated profile files such as `R12345678.yaml` or `s12345678.js`.
#[allow(clippy::unwrap_used)]
static REGEX_PROFILE_FILE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"^(?:[RLmrpg][a-zA-Z0-9]+\.yaml|s[a-zA-Z0-9]+\.js)$").unwrap());
static ACTIVATE_SELECTED_TASK: LazyLock<Mutex<Option<JoinHandle<()>>>> = LazyLock::new(|| Mutex::new(None)); static ACTIVATE_SELECTED_TASK: LazyLock<Mutex<Option<JoinHandle<()>>>> = LazyLock::new(|| Mutex::new(None));
static ACTIVATE_SELECTED_GENERATION: AtomicU64 = AtomicU64::new(0); static ACTIVATE_SELECTED_GENERATION: AtomicU64 = AtomicU64::new(0);
@@ -404,148 +399,6 @@ impl IProfiles {
.collect() .collect()
}) })
} }
/// 以 app 中的 profile 列表为准,删除不再需要的文件
pub async fn cleanup_orphaned_files(&self) -> Result<()> {
let profiles_dir = dirs::app_profiles_dir()?;
self.cleanup_orphaned_files_in(&profiles_dir).await
}
pub(super) async fn cleanup_orphaned_files_in(&self, profiles_dir: &Path) -> Result<()> {
if !profiles_dir.exists() {
return Ok(());
}
// An empty list may mean loading failed, so it is not safe evidence that every file is orphaned.
// https://github.com/clash-verge-rev/clash-verge-rev/issues/7577
if self.items.as_ref().is_none_or(|v| v.is_empty()) {
logging!(
warn,
Type::Config,
"Profile items 为空,跳过孤儿文件清理以避免误删活跃的配置文件"
);
return Ok(());
}
let active_files = self.get_all_active_files();
let protected_files = self.get_protected_global_files();
let mut total_files = 0;
let mut deleted_files = 0;
let mut failed_deletions = 0;
let mut dir_entries = tokio::fs::read_dir(&profiles_dir).await?;
while let Some(entry) = dir_entries.next_entry().await? {
let path = entry.path();
if !path.is_file() {
continue;
}
total_files += 1;
if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
&& Self::is_profile_file(file_name)
{
if protected_files.contains(file_name) {
logging!(debug, Type::Config, "保护全局扩展配置文件: {file_name}");
continue;
}
if !active_files.contains(file_name) {
match path.to_path_buf().remove_if_exists().await {
Ok(_) => {
deleted_files += 1;
logging!(debug, Type::Config, "已清理冗余文件: {file_name}");
}
Err(e) => {
failed_deletions += 1;
logging!(warn, Type::Config, "Warning: 清理文件失败: {file_name} - {e}");
}
}
}
}
}
logging!(
info,
Type::Config,
"Profile 文件清理完成: 总文件数={}, 删除文件数={}, 失败数={}",
total_files,
deleted_files,
failed_deletions
);
Ok(())
}
fn get_protected_global_files(&self) -> HashSet<String> {
let mut protected_files = HashSet::new();
protected_files.insert("Merge.yaml".into());
protected_files.insert("Script.js".into());
protected_files
}
fn get_all_active_files(&self) -> HashSet<&str> {
let mut active_files: HashSet<&str> = HashSet::new();
if let Some(items) = &self.items {
for item in items {
if let Some(file) = &item.file {
active_files.insert(file);
}
if let Some(itype) = &item.itype
&& (itype == "remote" || itype == "local")
&& let Some(option) = &item.option
{
if let Some(merge_uid) = &option.merge
&& let Ok(merge_item) = self.get_item(merge_uid)
&& let Some(file) = &merge_item.file
{
active_files.insert(file);
}
if let Some(script_uid) = &option.script
&& let Ok(script_item) = self.get_item(script_uid)
&& let Some(file) = &script_item.file
{
active_files.insert(file);
}
if let Some(rules_uid) = &option.rules
&& let Ok(rules_item) = self.get_item(rules_uid)
&& let Some(file) = &rules_item.file
{
active_files.insert(file);
}
if let Some(proxies_uid) = &option.proxies
&& let Ok(proxies_item) = self.get_item(proxies_uid)
&& let Some(file) = &proxies_item.file
{
active_files.insert(file);
}
if let Some(groups_uid) = &option.groups
&& let Ok(groups_item) = self.get_item(groups_uid)
&& let Some(file) = &groups_item.file
{
active_files.insert(file);
}
}
}
}
active_files
}
fn is_profile_file(filename: &str) -> bool {
REGEX_PROFILE_FILE.is_match(filename)
}
} }
// These helpers serialize asynchronous operations against committed profile data. // These helpers serialize asynchronous operations against committed profile data.