From fd2566ca2683d77120b7af217edbabc12eb1d3de Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Fri, 17 Apr 2026 11:09:49 -0400 Subject: [PATCH] refactor backend --- src-tauri/src/backend/advanced_clean.rs | 61 +++ src-tauri/src/backend/browser_clean.rs | 109 +++++ src-tauri/src/backend/disk_analysis.rs | 103 +++++ src-tauri/src/backend/fast_clean.rs | 161 +++++++ src-tauri/src/backend/memory_clean.rs | 62 +++ src-tauri/src/backend/mod.rs | 8 + src-tauri/src/backend/models.rs | 88 ++++ src-tauri/src/backend/state.rs | 6 + src-tauri/src/backend/utils.rs | 26 ++ src-tauri/src/cleaner.rs | 565 ------------------------ src-tauri/src/lib.rs | 81 ++-- 11 files changed, 671 insertions(+), 599 deletions(-) create mode 100644 src-tauri/src/backend/advanced_clean.rs create mode 100644 src-tauri/src/backend/browser_clean.rs create mode 100644 src-tauri/src/backend/disk_analysis.rs create mode 100644 src-tauri/src/backend/fast_clean.rs create mode 100644 src-tauri/src/backend/memory_clean.rs create mode 100644 src-tauri/src/backend/mod.rs create mode 100644 src-tauri/src/backend/models.rs create mode 100644 src-tauri/src/backend/state.rs create mode 100644 src-tauri/src/backend/utils.rs delete mode 100644 src-tauri/src/cleaner.rs diff --git a/src-tauri/src/backend/advanced_clean.rs b/src-tauri/src/backend/advanced_clean.rs new file mode 100644 index 0000000..87e7c74 --- /dev/null +++ b/src-tauri/src/backend/advanced_clean.rs @@ -0,0 +1,61 @@ +use std::fs; +use std::os::windows::process::CommandExt; +use std::path::Path; +use std::process::Command; + +pub async fn run_dism_cleanup() -> Result { + const CREATE_NO_WINDOW: u32 = 0x08000000; + + let output = Command::new("dism.exe") + .args(["/online", "/Cleanup-Image", "/StartComponentCleanup"]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok("系统组件清理完成。".into()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } +} + +pub async fn clean_thumbnails() -> Result { + let local_app_data = std::env::var("LOCALAPPDATA").map_err(|_| "无法获取 LocalAppData 路径")?; + let thumb_path = Path::new(&local_app_data).join("Microsoft\\Windows\\Explorer"); + + let mut count = 0; + if thumb_path.exists() { + if let Ok(entries) = fs::read_dir(thumb_path) { + for entry in entries.filter_map(|e| e.ok()) { + let name = entry.file_name().to_string_lossy().to_lowercase(); + if name.starts_with("thumbcache_") && name.ends_with(".db") { + if fs::remove_file(entry.path()).is_ok() { + count += 1; + } + } + } + } + } + + if count > 0 { + Ok(format!("成功清理 {} 个缩略图缓存文件。", count)) + } else { + Ok("未发现可清理的缩略图缓存,或文件正被系统占用。".into()) + } +} + +pub async fn disable_hibernation() -> Result { + const CREATE_NO_WINDOW: u32 = 0x08000000; + + let output = Command::new("powercfg.exe") + .args(["-h", "off"]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok("休眠模式已关闭,hiberfil.sys 已移除。".into()) + } else { + Err("执行失败,请确保以管理员身份运行。".into()) + } +} diff --git a/src-tauri/src/backend/browser_clean.rs b/src-tauri/src/backend/browser_clean.rs new file mode 100644 index 0000000..8375ce4 --- /dev/null +++ b/src-tauri/src/backend/browser_clean.rs @@ -0,0 +1,109 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::backend::fast_clean::clean_directory_contents; +use crate::backend::models::{BrowserProfile, BrowserScanResult, BrowserType, CleanResult}; +use crate::backend::utils::{format_size, get_dir_size_simple}; + +const BROWSER_CACHE_DIRS: &[&str] = &[ + "Cache", + "Code Cache", + "GPUCache", + "Media Cache", + "Service Worker/CacheStorage", + "Service Worker/ScriptCache", + "GrShaderCache", + "DawnCache", + "File System", + "blob_storage", +]; + +impl BrowserType { + fn get_user_data_path(&self) -> Result { + let local_app_data = std::env::var("LOCALAPPDATA").map_err(|_| "无法获取 LocalAppData 路径")?; + let base = Path::new(&local_app_data); + + match self { + BrowserType::Chrome => Ok(base.join("Google\\Chrome\\User Data")), + BrowserType::Edge => Ok(base.join("Microsoft\\Edge\\User Data")), + } + } +} + +pub async fn run_browser_scan(browser: BrowserType) -> Result { + let user_data_path = browser.get_user_data_path()?; + let local_state_path = user_data_path.join("Local State"); + + let mut profiles = Vec::new(); + let mut total_bytes = 0; + + if local_state_path.exists() { + let content = fs::read_to_string(local_state_path).map_err(|e| e.to_string())?; + let value: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?; + + if let Some(info_cache) = value + .get("profile") + .and_then(|profile| profile.get("info_cache")) + .and_then(|info| info.as_object()) + { + for (dir_name, info) in info_cache { + let profile_display_name = info.get("name").and_then(|name| name.as_str()).unwrap_or(dir_name); + let profile_path = user_data_path.join(dir_name); + + if profile_path.exists() { + let mut size = 0; + for sub_dir in BROWSER_CACHE_DIRS { + let target = profile_path.join(sub_dir); + if target.exists() { + size += get_dir_size_simple(&target); + } + } + + total_bytes += size; + profiles.push(BrowserProfile { + name: profile_display_name.to_string(), + path_name: dir_name.clone(), + cache_size: size, + cache_size_str: format_size(size), + }); + } + } + } + } + + Ok(BrowserScanResult { + profiles, + total_size: format_size(total_bytes), + }) +} + +pub async fn run_browser_clean( + browser: BrowserType, + profile_paths: Vec, +) -> Result { + let user_data_path = browser.get_user_data_path()?; + let mut total_freed = 0; + let mut success_count = 0; + let mut fail_count = 0; + + for profile_dir in profile_paths { + let profile_path = user_data_path.join(&profile_dir); + if profile_path.exists() { + for sub_dir in BROWSER_CACHE_DIRS { + let target = profile_path.join(sub_dir); + if target.exists() { + let (freed, success, fail) = clean_directory_contents(&target, None); + total_freed += freed; + success_count += success; + fail_count += fail; + } + } + } + } + + Ok(CleanResult { + total_freed: format_size(total_freed), + success_count, + fail_count, + }) +} diff --git a/src-tauri/src/backend/disk_analysis.rs b/src-tauri/src/backend/disk_analysis.rs new file mode 100644 index 0000000..bccd6cc --- /dev/null +++ b/src-tauri/src/backend/disk_analysis.rs @@ -0,0 +1,103 @@ +use std::collections::HashMap; +use std::fs; +use std::os::windows::process::CommandExt; +use std::path::Path; +use std::process::Command; + +use tauri::Emitter; + +use crate::backend::models::{FileTreeNode, ScanProgress}; +use crate::backend::state::DiskState; +use crate::backend::utils::format_size; + +pub async fn run_full_scan(root_path: String, state: &DiskState, app_handle: tauri::AppHandle) { + use jwalk::WalkDir; + + let mut dir_sizes = HashMap::new(); + let root = Path::new(&root_path); + let mut file_count = 0; + + for entry in WalkDir::new(root).skip_hidden(false).into_iter().filter_map(|e| e.ok()) { + if entry.file_type.is_file() { + file_count += 1; + + if file_count % 2000 == 0 { + let _ = app_handle.emit( + "scan-progress", + ScanProgress { + file_count, + current_path: entry.parent_path().to_string_lossy().to_string(), + }, + ); + } + + let size = entry.metadata().map(|m| m.len()).unwrap_or(0); + let mut current_path = entry.parent_path().to_path_buf(); + while current_path.starts_with(root) { + let path_str = current_path.to_string_lossy().to_string(); + *dir_sizes.entry(path_str).or_insert(0) += size; + if current_path == root { + break; + } + if let Some(parent) = current_path.parent() { + current_path = parent.to_path_buf(); + } else { + break; + } + } + } + } + + let mut state_dirs = state.dir_sizes.lock().unwrap(); + *state_dirs = dir_sizes; +} + +pub fn get_children(parent_path: String, state: &DiskState) -> Vec { + let dir_sizes = state.dir_sizes.lock().unwrap(); + let mut results = Vec::new(); + let parent_size = *dir_sizes.get(&parent_path).unwrap_or(&1); + + if let Ok(entries) = fs::read_dir(Path::new(&parent_path)) { + for entry in entries.filter_map(|e| e.ok()) { + let path = entry.path(); + let path_str = path.to_string_lossy().to_string(); + let is_dir = path.is_dir(); + let name = entry.file_name().to_string_lossy().to_string(); + + let size = if is_dir { + *dir_sizes.get(&path_str).unwrap_or(&0) + } else { + entry.metadata().map(|m| m.len()).unwrap_or(0) + }; + + if size > 0 || !is_dir { + results.push(FileTreeNode { + name, + path: path_str, + is_dir, + size, + size_str: format_size(size), + percent: (size as f64 / parent_size as f64 * 100.0) as f32, + file_count: 0, + has_children: is_dir, + }); + } + } + } + + results.sort_by(|a, b| b.size.cmp(&a.size)); + results +} + +pub async fn open_explorer(path: String) -> Result<(), String> { + const CREATE_NO_WINDOW: u32 = 0x08000000; + + Command::new("explorer.exe") + .arg("/select,") + .arg(&path) + .creation_flags(CREATE_NO_WINDOW) + .spawn() + .map_err(|e| e.to_string())?; + + Ok(()) +} diff --git a/src-tauri/src/backend/fast_clean.rs b/src-tauri/src/backend/fast_clean.rs new file mode 100644 index 0000000..a092d6c --- /dev/null +++ b/src-tauri/src/backend/fast_clean.rs @@ -0,0 +1,161 @@ +use std::fs; +use std::path::Path; +use std::time::{Duration, SystemTime}; + +use crate::backend::models::{CleanResult, CleaningConfig, FastScanResult, ScanItem}; +use crate::backend::utils::format_size; + +fn get_fast_cleaning_configs() -> Vec { + let mut configs = Vec::new(); + + if let Ok(temp) = std::env::var("TEMP") { + configs.push(CleaningConfig::new("用户临时文件", &temp, None, true)); + } + + configs.push(CleaningConfig::new("系统临时文件", "C:\\Windows\\Temp", None, true)); + configs.push(CleaningConfig::new( + "Windows 更新残留", + "C:\\Windows\\SoftwareDistribution\\Download", + Some(10), + true, + )); + configs.push(CleaningConfig::new( + "内核转储文件", + "C:\\Windows\\LiveKernelReports", + None, + false, + )); + + configs +} + +pub async fn run_fast_scan() -> FastScanResult { + let configs = get_fast_cleaning_configs(); + let mut items = Vec::new(); + let mut total_bytes = 0; + let mut total_count = 0; + + for config in configs { + let (size, count) = get_dir_stats(Path::new(&config.path), config.filter_days); + items.push(ScanItem { + name: config.name, + path: config.path, + size, + count, + enabled: config.default_enabled, + }); + total_bytes += size; + total_count += count; + } + + FastScanResult { + items, + total_size: format_size(total_bytes), + total_count, + } +} + +fn get_dir_stats(path: &Path, filter_days: Option) -> (u64, u32) { + if !path.exists() { + return (0, 0); + } + + let mut size = 0; + let mut count = 0; + let now = SystemTime::now(); + let dur = filter_days.map(|days| Duration::from_secs(days * 24 * 3600)); + + for entry in walkdir::WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { + if entry.file_type().is_file() { + let mut allowed = true; + if let (Some(filter_duration), Ok(metadata)) = (dur, entry.metadata()) { + if let Ok(modified_time) = metadata.modified() { + if let Ok(elapsed) = now.duration_since(modified_time) { + if elapsed < filter_duration { + allowed = false; + } + } + } + } + + if allowed { + size += entry.metadata().map(|m| m.len()).unwrap_or(0); + count += 1; + } + } + } + + (size, count) +} + +pub async fn run_fast_clean(selected_paths: Vec) -> Result { + let configs = get_fast_cleaning_configs(); + let mut success_count = 0; + let mut fail_count = 0; + let mut total_freed = 0; + + for config in configs { + if selected_paths.contains(&config.path) { + let path = Path::new(&config.path); + if path.exists() { + let (freed, success, fail) = clean_directory_contents(path, config.filter_days); + total_freed += freed; + success_count += success; + fail_count += fail; + } + } + } + + Ok(CleanResult { + total_freed: format_size(total_freed), + success_count, + fail_count, + }) +} + +pub fn clean_directory_contents(path: &Path, filter_days: Option) -> (u64, u32, u32) { + let mut freed = 0; + let mut success = 0; + let mut fail = 0; + let now = SystemTime::now(); + let dur = filter_days.map(|days| Duration::from_secs(days * 24 * 3600)); + + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.filter_map(|e| e.ok()) { + let entry_path = entry.path(); + let metadata = entry.metadata(); + let size = metadata.as_ref().map(|m| m.len()).unwrap_or(0); + + if let (Some(filter_duration), Ok(metadata)) = (dur, &metadata) { + if let Ok(modified_time) = metadata.modified() { + if let Ok(elapsed) = now.duration_since(modified_time) { + if elapsed < filter_duration { + continue; + } + } + } + } + + if entry_path.is_file() { + if fs::remove_file(&entry_path).is_ok() { + freed += size; + success += 1; + } else { + fail += 1; + } + } else if entry_path.is_dir() { + let (dir_freed, dir_success, dir_fail) = + clean_directory_contents(&entry_path, filter_days); + freed += dir_freed; + success += dir_success; + fail += dir_fail; + + if fs::remove_dir(&entry_path).is_ok() { + success += 1; + } + } + } + } + + (freed, success, fail) +} diff --git a/src-tauri/src/backend/memory_clean.rs b/src-tauri/src/backend/memory_clean.rs new file mode 100644 index 0000000..e27aaa8 --- /dev/null +++ b/src-tauri/src/backend/memory_clean.rs @@ -0,0 +1,62 @@ +use sysinfo::{ProcessesToUpdate, System}; + +use crate::backend::models::MemoryStats; + +pub fn get_memory_stats() -> MemoryStats { + let mut sys = System::new_all(); + sys.refresh_memory(); + + let total = sys.total_memory(); + let used = sys.used_memory(); + let free = total.saturating_sub(used); + let percent = (used as f32 / total as f32) * 100.0; + + MemoryStats { + total, + used, + free, + percent, + } +} + +pub async fn run_memory_clean() -> Result { + use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Win32::System::ProcessStatus::EmptyWorkingSet; + use windows_sys::Win32::System::Threading::{ + OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_SET_QUOTA, + }; + + let before = get_memory_stats().used; + + let mut sys = System::new_all(); + sys.refresh_processes(ProcessesToUpdate::All, true); + + for (pid, _) in sys.processes() { + let pid_u32 = pid.as_u32(); + unsafe { + let handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_QUOTA, 0, pid_u32); + if handle != std::ptr::null_mut() { + EmptyWorkingSet(handle); + CloseHandle(handle); + } + } + } + + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + let after = get_memory_stats().used; + Ok(before.saturating_sub(after)) +} + +pub async fn run_deep_memory_clean() -> Result { + use windows_sys::Win32::System::Memory::SetSystemFileCacheSize; + + let before = get_memory_stats().used; + + unsafe { + SetSystemFileCacheSize(usize::MAX, usize::MAX, 0); + } + + let after = get_memory_stats().used; + Ok(before.saturating_sub(after)) +} diff --git a/src-tauri/src/backend/mod.rs b/src-tauri/src/backend/mod.rs new file mode 100644 index 0000000..181b8a4 --- /dev/null +++ b/src-tauri/src/backend/mod.rs @@ -0,0 +1,8 @@ +pub mod advanced_clean; +pub mod browser_clean; +pub mod disk_analysis; +pub mod fast_clean; +pub mod memory_clean; +pub mod models; +pub mod state; +pub mod utils; diff --git a/src-tauri/src/backend/models.rs b/src-tauri/src/backend/models.rs new file mode 100644 index 0000000..f49492c --- /dev/null +++ b/src-tauri/src/backend/models.rs @@ -0,0 +1,88 @@ +use serde::Serialize; + +#[derive(Serialize, Clone)] +pub struct FileTreeNode { + pub name: String, + pub path: String, + pub is_dir: bool, + pub size: u64, + pub size_str: String, + pub percent: f32, + pub file_count: u32, + pub has_children: bool, +} + +#[derive(Serialize, Clone)] +pub struct ScanProgress { + pub file_count: u64, + pub current_path: String, +} + +#[derive(Clone)] +pub struct CleaningConfig { + pub name: String, + pub path: String, + pub filter_days: Option, + pub default_enabled: bool, +} + +impl CleaningConfig { + pub fn new(name: &str, path: &str, filter_days: Option, default_enabled: bool) -> Self { + Self { + name: name.into(), + path: path.into(), + filter_days, + default_enabled, + } + } +} + +#[derive(Serialize, Clone)] +pub struct ScanItem { + pub name: String, + pub path: String, + pub size: u64, + pub count: u32, + pub enabled: bool, +} + +#[derive(Serialize)] +pub struct FastScanResult { + pub items: Vec, + pub total_size: String, + pub total_count: u32, +} + +#[derive(Serialize)] +pub struct CleanResult { + pub total_freed: String, + pub success_count: u32, + pub fail_count: u32, +} + +#[derive(Serialize, Clone)] +pub struct BrowserProfile { + pub name: String, + pub path_name: String, + pub cache_size: u64, + pub cache_size_str: String, +} + +#[derive(Serialize)] +pub struct BrowserScanResult { + pub profiles: Vec, + pub total_size: String, +} + +pub enum BrowserType { + Chrome, + Edge, +} + +#[derive(Serialize, Clone)] +pub struct MemoryStats { + pub total: u64, + pub used: u64, + pub free: u64, + pub percent: f32, +} diff --git a/src-tauri/src/backend/state.rs b/src-tauri/src/backend/state.rs new file mode 100644 index 0000000..c8836c7 --- /dev/null +++ b/src-tauri/src/backend/state.rs @@ -0,0 +1,6 @@ +use std::collections::HashMap; +use std::sync::Mutex; + +pub struct DiskState { + pub dir_sizes: Mutex>, +} diff --git a/src-tauri/src/backend/utils.rs b/src-tauri/src/backend/utils.rs new file mode 100644 index 0000000..678c805 --- /dev/null +++ b/src-tauri/src/backend/utils.rs @@ -0,0 +1,26 @@ +use std::path::Path; + +pub fn format_size(size: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + + if size >= GB { + format!("{:.2} GB", size as f64 / GB as f64) + } else if size >= MB { + format!("{:.2} MB", size as f64 / MB as f64) + } else if size >= KB { + format!("{:.2} KB", size as f64 / KB as f64) + } else { + format!("{} B", size) + } +} + +pub fn get_dir_size_simple(path: &Path) -> u64 { + walkdir::WalkDir::new(path) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0)) + .sum() +} diff --git a/src-tauri/src/cleaner.rs b/src-tauri/src/cleaner.rs deleted file mode 100644 index 6a09bb0..0000000 --- a/src-tauri/src/cleaner.rs +++ /dev/null @@ -1,565 +0,0 @@ -use std::fs; -use std::path::Path; -use std::time::{SystemTime, Duration}; -use serde::Serialize; -use std::collections::HashMap; -use std::sync::Mutex; -use std::process::Command; -use std::os::windows::process::CommandExt; - -// 存储全盘扫描后的结果 -pub struct DiskState { - pub dir_sizes: Mutex>, - // pub file_info: Mutex>, -} - -#[derive(Serialize, Clone)] -pub struct FileTreeNode { - pub name: String, - pub path: String, - pub is_dir: bool, - pub size: u64, - pub size_str: String, - pub percent: f32, - pub file_count: u32, - pub has_children: bool, -} - -pub fn format_size(size: u64) -> String { - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - if size >= GB { format!("{:.2} GB", size as f64 / GB as f64) } - else if size >= MB { format!("{:.2} MB", size as f64 / MB as f64) } - else if size >= KB { format!("{:.2} KB", size as f64 / KB as f64) } - else { format!("{} B", size) } -} - -// --- 高级清理功能实现 --- - -// 1. 系统组件清理 (DISM) -pub async fn run_dism_cleanup() -> Result { - const CREATE_NO_WINDOW: u32 = 0x08000000; - let output = Command::new("dism.exe") - .args(&["/online", "/Cleanup-Image", "/StartComponentCleanup"]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .map_err(|e| e.to_string())?; - - if output.status.success() { - Ok("系统组件清理完成。".into()) - } else { - Err(String::from_utf8_lossy(&output.stderr).to_string()) - } -} - -// 2. 清理缩略图缓存 -pub async fn clean_thumbnails() -> Result { - let local_app_data = std::env::var("LOCALAPPDATA").map_err(|_| "无法获取 LocalAppData 路径")?; - let thumb_path = Path::new(&local_app_data).join("Microsoft\\Windows\\Explorer"); - - let mut count = 0; - if thumb_path.exists() { - if let Ok(entries) = fs::read_dir(thumb_path) { - for entry in entries.filter_map(|e| e.ok()) { - let name = entry.file_name().to_string_lossy().to_lowercase(); - if name.starts_with("thumbcache_") && name.ends_with(".db") { - // 缩略图文件通常被 Explorer 占用,这里尝试删除,失败也继续 - if fs::remove_file(entry.path()).is_ok() { - count += 1; - } - } - } - } - } - - if count > 0 { - Ok(format!("成功清理 {} 个缩略图缓存文件。", count)) - } else { - Ok("未发现可清理的缩略图缓存,或文件正被系统占用。".into()) - } -} - -// 3. 关闭休眠文件 -pub async fn disable_hibernation() -> Result { - const CREATE_NO_WINDOW: u32 = 0x08000000; - let output = Command::new("powercfg.exe") - .args(&["-h", "off"]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .map_err(|e| e.to_string())?; - - if output.status.success() { - Ok("休眠模式已关闭,hiberfil.sys 已移除。".into()) - } else { - Err("执行失败,请确保以管理员身份运行。".into()) - } -} - -// --- 原有逻辑保持 (磁盘树等) --- - -#[derive(Serialize, Clone)] -pub struct ScanProgress { - pub file_count: u64, - pub current_path: String, -} - -pub async fn run_full_scan(root_path: String, state: &DiskState, app_handle: tauri::AppHandle) { - use jwalk::WalkDir; - use tauri::Emitter; - - let mut dir_sizes = HashMap::new(); - let root = Path::new(&root_path); - let mut file_count = 0; - - for entry in WalkDir::new(root).skip_hidden(false).into_iter().filter_map(|e| e.ok()) { - if entry.file_type.is_file() { - file_count += 1; - - // 节流推送进度:每 2000 个文件推送一次,避免 IPC 过载 - if file_count % 2000 == 0 { - let _ = app_handle.emit("scan-progress", ScanProgress { - file_count, - current_path: entry.parent_path().to_string_lossy().to_string(), - }); - } - - let size = entry.metadata().map(|m| m.len()).unwrap_or(0); - let mut current_path = entry.parent_path().to_path_buf(); - while current_path.starts_with(root) { - let path_str = current_path.to_string_lossy().to_string(); - *dir_sizes.entry(path_str).or_insert(0) += size; - if current_path == root { break; } - if let Some(parent) = current_path.parent() { current_path = parent.to_path_buf(); } else { break; } - } - } - } - let mut state_dirs = state.dir_sizes.lock().unwrap(); - *state_dirs = dir_sizes; -} - -pub fn get_children(parent_path: String, state: &DiskState) -> Vec { - let dir_sizes = state.dir_sizes.lock().unwrap(); - let mut results = Vec::new(); - let parent_size = *dir_sizes.get(&parent_path).unwrap_or(&1); - - if let Ok(entries) = fs::read_dir(Path::new(&parent_path)) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - let path_str = path.to_string_lossy().to_string(); - let is_dir = path.is_dir(); - let name = entry.file_name().to_string_lossy().to_string(); - - let size = if is_dir { - *dir_sizes.get(&path_str).unwrap_or(&0) - } else { - entry.metadata().map(|m| m.len()).unwrap_or(0) - }; - - if size > 0 || !is_dir { - results.push(FileTreeNode { - name, - path: path_str, - is_dir, - size, - size_str: format_size(size), - percent: (size as f64 / parent_size as f64 * 100.0) as f32, - file_count: 0, - has_children: is_dir, - }); - } - } - } - results.sort_by(|a, b| b.size.cmp(&a.size)); - results -} - -pub async fn open_explorer(path: String) -> Result<(), String> { - const CREATE_NO_WINDOW: u32 = 0x08000000; - // 使用 /select, 参数可以在打开目录的同时选中目标 - Command::new("explorer.exe") - .arg("/select,") - .arg(&path) - .creation_flags(CREATE_NO_WINDOW) - .spawn() - .map_err(|e| e.to_string())?; - Ok(()) -} - -// --- 快速模式配置与逻辑 --- - -#[derive(Clone)] -pub struct CleaningConfig { - pub name: String, - pub path: String, - pub filter_days: Option, - pub default_enabled: bool, -} - -impl CleaningConfig { - fn new(name: &str, path: &str, filter_days: Option, default_enabled: bool) -> Self { - Self { name: name.into(), path: path.into(), filter_days, default_enabled } - } -} - -/// 获取当前所有快速清理项的配置 -fn get_fast_cleaning_configs() -> Vec { - let mut configs = Vec::new(); - - // 1. 用户临时文件 - if let Ok(t) = std::env::var("TEMP") { - configs.push(CleaningConfig::new("用户临时文件", &t, None, true)); - } - - // 2. 系统临时文件 - configs.push(CleaningConfig::new("系统临时文件", "C:\\Windows\\Temp", None, true)); - - // 3. Windows 更新残留 (通常建议清理 10 天前的) - configs.push(CleaningConfig::new("Windows 更新残留", "C:\\Windows\\SoftwareDistribution\\Download", Some(10), true)); - - // 4. 内核转储文件 - configs.push(CleaningConfig::new("内核转储文件", "C:\\Windows\\LiveKernelReports", None, false)); - - configs -} - -#[derive(Serialize, Clone)] -pub struct ScanItem { - pub name: String, - pub path: String, - pub size: u64, - pub count: u32, - pub enabled: bool, -} - -#[derive(Serialize)] -pub struct FastScanResult { pub items: Vec, total_size: String, total_count: u32 } - -pub async fn run_fast_scan() -> FastScanResult { - let configs = get_fast_cleaning_configs(); - let mut items = Vec::new(); - let mut total_bytes = 0; - let mut total_count = 0; - - for config in configs { - let (size, count) = get_dir_stats(Path::new(&config.path), config.filter_days); - items.push(ScanItem { - name: config.name, - path: config.path, - size, - count, - enabled: config.default_enabled, - }); - total_bytes += size; - total_count += count; - } - - FastScanResult { - items, - total_size: format_size(total_bytes), - total_count - } -} - -fn get_dir_stats(path: &Path, filter_days: Option) -> (u64, u32) { - if !path.exists() { return (0, 0); } - let mut size = 0; - let mut count = 0; - let now = SystemTime::now(); - let dur = filter_days.map(|d| Duration::from_secs(d * 24 * 3600)); - - for entry in walkdir::WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { - if entry.file_type().is_file() { - let mut ok = true; - if let (Some(d), Ok(m)) = (dur, entry.metadata()) { - if let Ok(mod_t) = m.modified() { - if let Ok(el) = now.duration_since(mod_t) { - if el < d { ok = false; } - } - } - } - if ok { - size += entry.metadata().map(|m| m.len()).unwrap_or(0); - count += 1; - } - } - } - (size, count) -} - -#[derive(Serialize)] -pub struct CleanResult { - pub total_freed: String, - pub success_count: u32, - pub fail_count: u32, -} - -pub async fn run_fast_clean(selected_paths: Vec) -> Result { - let configs = get_fast_cleaning_configs(); - let mut success_count = 0; - let mut fail_count = 0; - let mut total_freed: u64 = 0; - - for config in configs { - if selected_paths.contains(&config.path) { - let path = Path::new(&config.path); - if path.exists() { - let (freed, s, f) = clean_directory_contents(path, config.filter_days); - total_freed += freed; - success_count += s; - fail_count += f; - } - } - } - - Ok(CleanResult { - total_freed: format_size(total_freed), - success_count, - fail_count, - }) -} - -fn clean_directory_contents(path: &Path, filter_days: Option) -> (u64, u32, u32) { - let mut freed = 0; - let mut success = 0; - let mut fail = 0; - let now = SystemTime::now(); - let dur = filter_days.map(|d| Duration::from_secs(d * 24 * 3600)); - - if let Ok(entries) = fs::read_dir(path) { - for entry in entries.filter_map(|e| e.ok()) { - let entry_path = entry.path(); - let metadata = entry.metadata(); - let size = metadata.as_ref().map(|m| m.len()).unwrap_or(0); - - // 检查过滤逻辑 (如果设置了天数) - if let (Some(d), Ok(m)) = (dur, &metadata) { - if let Ok(mod_t) = m.modified() { - if let Ok(el) = now.duration_since(mod_t) { - if el < d { continue; } - } - } - } - - if entry_path.is_file() { - if fs::remove_file(&entry_path).is_ok() { - freed += size; - success += 1; - } else { - fail += 1; - } - } else if entry_path.is_dir() { - // 递归清理子目录 - let (f, s, fl) = clean_directory_contents(&entry_path, filter_days); - freed += f; - success += s; - fail += fl; - // 尝试删除已清空的目录 (如果它本身不是根清理目录且已过期) - if fs::remove_dir(&entry_path).is_ok() { - success += 1; - } else { - // 目录可能因为包含未过期的文件而无法删除,这是正常的 - } - } - } - } - (freed, success, fail) -} - -// --- 浏览器清理逻辑 --- - -const BROWSER_CACHE_DIRS: &[&str] = &[ - "Cache", - "Code Cache", - "GPUCache", - "Media Cache", - "Service Worker/CacheStorage", - "Service Worker/ScriptCache", - "GrShaderCache", - "DawnCache", - "File System", - "blob_storage" -]; - -#[derive(Serialize, Clone)] -pub struct BrowserProfile { - pub name: String, - pub path_name: String, - pub cache_size: u64, - pub cache_size_str: String, -} - -#[derive(Serialize)] -pub struct BrowserScanResult { - pub profiles: Vec, - pub total_size: String, -} - -pub enum BrowserType { - Chrome, - Edge, -} - -impl BrowserType { - fn get_user_data_path(&self) -> Result { - let local_app_data = std::env::var("LOCALAPPDATA").map_err(|_| "无法获取 LocalAppData 路径")?; - let base = std::path::Path::new(&local_app_data); - match self { - BrowserType::Chrome => Ok(base.join("Google\\Chrome\\User Data")), - BrowserType::Edge => Ok(base.join("Microsoft\\Edge\\User Data")), - } - } -} - -pub async fn run_browser_scan(browser: BrowserType) -> Result { - let user_data_path = browser.get_user_data_path()?; - let local_state_path = user_data_path.join("Local State"); - - let mut profiles = Vec::new(); - let mut total_bytes = 0; - - if local_state_path.exists() { - let content = fs::read_to_string(local_state_path).map_err(|e| e.to_string())?; - let v: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?; - - if let Some(info_cache) = v.get("profile").and_then(|p| p.get("info_cache")).and_then(|i| i.as_object()) { - for (dir_name, info) in info_cache { - let profile_display_name = info.get("name").and_then(|n| n.as_str()).unwrap_or(dir_name); - let profile_path = user_data_path.join(dir_name); - - if profile_path.exists() { - let mut size = 0; - // 扫描配置的缓存目录 - for sub in BROWSER_CACHE_DIRS { - let target = profile_path.join(sub); - if target.exists() { - size += get_dir_size_simple(&target); - } - } - - total_bytes += size; - profiles.push(BrowserProfile { - name: profile_display_name.to_string(), - path_name: dir_name.clone(), - cache_size: size, - cache_size_str: format_size(size), - }); - } - } - } - } - - Ok(BrowserScanResult { - profiles, - total_size: format_size(total_bytes), - }) -} - -fn get_dir_size_simple(path: &std::path::Path) -> u64 { - walkdir::WalkDir::new(path) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_file()) - .map(|e| e.metadata().map(|m| m.len()).unwrap_or(0)) - .sum() -} - -pub async fn run_browser_clean(browser: BrowserType, profile_paths: Vec) -> Result { - let user_data_path = browser.get_user_data_path()?; - let mut total_freed = 0; - let mut success_count = 0; - let mut fail_count = 0; - - for profile_dir in profile_paths { - let profile_path = user_data_path.join(&profile_dir); - if profile_path.exists() { - // 清理配置的缓存目录 - for sub in BROWSER_CACHE_DIRS { - let target = profile_path.join(sub); - if target.exists() { - let (f, s, fl) = clean_directory_contents(&target, None); - total_freed += f; - success_count += s; - fail_count += fl; - } - } - } - } - - Ok(CleanResult { - total_freed: format_size(total_freed), - success_count, - fail_count, - }) -} - -// --- 内存清理逻辑 --- - -use sysinfo::{System, ProcessesToUpdate}; - -#[derive(Serialize, Clone)] -pub struct MemoryStats { - pub total: u64, - pub used: u64, - pub free: u64, - pub percent: f32, -} - -/// 获取当前系统内存状态 -pub fn get_memory_stats() -> MemoryStats { - let mut sys = System::new_all(); - sys.refresh_memory(); - - let total = sys.total_memory(); - let used = sys.used_memory(); - let free = total.saturating_sub(used); - let percent = (used as f32 / total as f32) * 100.0; - - MemoryStats { total, used, free, percent } -} - -/// 执行内存压缩 (Empty Working Set) -pub async fn run_memory_clean() -> Result { - use windows_sys::Win32::System::ProcessStatus::EmptyWorkingSet; - use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_SET_QUOTA}; - use windows_sys::Win32::Foundation::CloseHandle; - - let before = get_memory_stats().used; - - let mut sys = System::new_all(); - sys.refresh_processes(ProcessesToUpdate::All, true); - - for (pid, _) in sys.processes() { - let pid_u32 = pid.as_u32(); - unsafe { - let handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_QUOTA, 0, pid_u32); - if handle != std::ptr::null_mut() { - EmptyWorkingSet(handle); - CloseHandle(handle); - } - } - } - - // 给系统一点点时间反应 - tokio::time::sleep(std::time::Duration::from_millis(500)).await; - - let after = get_memory_stats().used; - let freed = before.saturating_sub(after); - Ok(freed) -} - -/// 深度内存清理 (Standby List / System Cache) -pub async fn run_deep_memory_clean() -> Result { - use windows_sys::Win32::System::Memory::SetSystemFileCacheSize; - - let before = get_memory_stats().used; - - unsafe { - // -1 (usize::MAX) 表示清空系统文件缓存 - SetSystemFileCacheSize(usize::MAX, usize::MAX, 0); - } - - let after = get_memory_stats().used; - let freed = before.saturating_sub(after); - Ok(freed) -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9df53ce..0767e97 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,89 +1,102 @@ -mod cleaner; -use tauri::State; use std::collections::HashMap; use std::sync::Mutex; +use tauri::State; + +mod backend; #[tauri::command] -async fn start_fast_scan() -> cleaner::FastScanResult { - cleaner::run_fast_scan().await +async fn start_fast_scan() -> backend::models::FastScanResult { + backend::fast_clean::run_fast_scan().await } #[tauri::command] -async fn start_fast_clean(selected_paths: Vec) -> Result { - cleaner::run_fast_clean(selected_paths).await +async fn start_fast_clean(selected_paths: Vec) -> Result { + backend::fast_clean::run_fast_clean(selected_paths).await } #[tauri::command] -async fn start_full_disk_scan(state: State<'_, cleaner::DiskState>, app_handle: tauri::AppHandle) -> Result<(), String> { - cleaner::run_full_scan("C:\\".to_string(), &state, app_handle).await; +async fn start_full_disk_scan( + state: State<'_, backend::state::DiskState>, + app_handle: tauri::AppHandle, +) -> Result<(), String> { + backend::disk_analysis::run_full_scan("C:\\".to_string(), &state, app_handle).await; Ok(()) } #[tauri::command] -async fn get_tree_children(path: String, state: State<'_, cleaner::DiskState>) -> Result, String> { - Ok(cleaner::get_children(path, &state)) +async fn get_tree_children( + path: String, + state: State<'_, backend::state::DiskState>, +) -> Result, String> { + Ok(backend::disk_analysis::get_children(path, &state)) } #[tauri::command] async fn open_in_explorer(path: String) -> Result<(), String> { - cleaner::open_explorer(path).await + backend::disk_analysis::open_explorer(path).await } -// --- 高级清理命令 --- - #[tauri::command] async fn clean_system_components() -> Result { - cleaner::run_dism_cleanup().await + backend::advanced_clean::run_dism_cleanup().await } #[tauri::command] async fn clean_thumbnails() -> Result { - cleaner::clean_thumbnails().await + backend::advanced_clean::clean_thumbnails().await } #[tauri::command] async fn disable_hibernation() -> Result { - cleaner::disable_hibernation().await -} - -// --- 浏览器清理命令 --- - -#[tauri::command] -async fn start_browser_scan(browser: String) -> Result { - let b_type = if browser == "chrome" { cleaner::BrowserType::Chrome } else { cleaner::BrowserType::Edge }; - cleaner::run_browser_scan(b_type).await + backend::advanced_clean::disable_hibernation().await } #[tauri::command] -async fn start_browser_clean(browser: String, profiles: Vec) -> Result { - let b_type = if browser == "chrome" { cleaner::BrowserType::Chrome } else { cleaner::BrowserType::Edge }; - cleaner::run_browser_clean(b_type, profiles).await +async fn start_browser_scan(browser: String) -> Result { + let browser_type = if browser == "chrome" { + backend::models::BrowserType::Chrome + } else { + backend::models::BrowserType::Edge + }; + + backend::browser_clean::run_browser_scan(browser_type).await } -// --- 内存清理命令 --- +#[tauri::command] +async fn start_browser_clean( + browser: String, + profiles: Vec, +) -> Result { + let browser_type = if browser == "chrome" { + backend::models::BrowserType::Chrome + } else { + backend::models::BrowserType::Edge + }; + + backend::browser_clean::run_browser_clean(browser_type, profiles).await +} #[tauri::command] -async fn get_memory_stats() -> cleaner::MemoryStats { - cleaner::get_memory_stats() +async fn get_memory_stats() -> backend::models::MemoryStats { + backend::memory_clean::get_memory_stats() } #[tauri::command] async fn run_memory_clean() -> Result { - cleaner::run_memory_clean().await + backend::memory_clean::run_memory_clean().await } #[tauri::command] async fn run_deep_memory_clean() -> Result { - cleaner::run_deep_memory_clean().await + backend::memory_clean::run_deep_memory_clean().await } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) - .manage(cleaner::DiskState { + .manage(backend::state::DiskState { dir_sizes: Mutex::new(HashMap::new()), - // file_info: Mutex::new(HashMap::new()), }) .invoke_handler(tauri::generate_handler![ start_fast_scan,