mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 05:40:47 +08:00
fix(profile): preserve files until explicit deletion
This commit is contained in:
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user