refactor backend

This commit is contained in:
Julian Freeman
2026-04-17 11:09:49 -04:00
parent 40215bcbcb
commit fd2566ca26
11 changed files with 671 additions and 599 deletions

View File

@@ -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<String, String> {
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<String, String> {
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<String, String> {
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())
}
}

View File

@@ -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<PathBuf, String> {
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<BrowserScanResult, String> {
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<String>,
) -> Result<CleanResult, String> {
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,
})
}

View File

@@ -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<FileTreeNode> {
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(())
}

View File

@@ -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<CleaningConfig> {
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>) -> (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<String>) -> Result<CleanResult, String> {
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>) -> (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)
}

View File

@@ -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<u64, String> {
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<u64, String> {
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))
}

View File

@@ -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;

View File

@@ -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<u64>,
pub default_enabled: bool,
}
impl CleaningConfig {
pub fn new(name: &str, path: &str, filter_days: Option<u64>, 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<ScanItem>,
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<BrowserProfile>,
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,
}

View File

@@ -0,0 +1,6 @@
use std::collections::HashMap;
use std::sync::Mutex;
pub struct DiskState {
pub dir_sizes: Mutex<HashMap<String, u64>>,
}

View File

@@ -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()
}

View File

@@ -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<HashMap<String, u64>>,
// pub file_info: Mutex<HashMap<String, (u64, u32)>>,
}
#[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<String, String> {
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<String, String> {
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<String, String> {
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<FileTreeNode> {
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<u64>,
pub default_enabled: bool,
}
impl CleaningConfig {
fn new(name: &str, path: &str, filter_days: Option<u64>, default_enabled: bool) -> Self {
Self { name: name.into(), path: path.into(), filter_days, default_enabled }
}
}
/// 获取当前所有快速清理项的配置
fn get_fast_cleaning_configs() -> Vec<CleaningConfig> {
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<ScanItem>, 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>) -> (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<String>) -> Result<CleanResult, String> {
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>) -> (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<BrowserProfile>,
pub total_size: String,
}
pub enum BrowserType {
Chrome,
Edge,
}
impl BrowserType {
fn get_user_data_path(&self) -> Result<std::path::PathBuf, String> {
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<BrowserScanResult, String> {
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<String>) -> Result<CleanResult, String> {
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<u64, String> {
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<u64, String> {
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)
}

View File

@@ -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<String>) -> Result<cleaner::CleanResult, String> {
cleaner::run_fast_clean(selected_paths).await
async fn start_fast_clean(selected_paths: Vec<String>) -> Result<backend::models::CleanResult, String> {
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<Vec<cleaner::FileTreeNode>, String> {
Ok(cleaner::get_children(path, &state))
async fn get_tree_children(
path: String,
state: State<'_, backend::state::DiskState>,
) -> Result<Vec<backend::models::FileTreeNode>, 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<String, String> {
cleaner::run_dism_cleanup().await
backend::advanced_clean::run_dism_cleanup().await
}
#[tauri::command]
async fn clean_thumbnails() -> Result<String, String> {
cleaner::clean_thumbnails().await
backend::advanced_clean::clean_thumbnails().await
}
#[tauri::command]
async fn disable_hibernation() -> Result<String, String> {
cleaner::disable_hibernation().await
}
// --- 浏览器清理命令 ---
#[tauri::command]
async fn start_browser_scan(browser: String) -> Result<cleaner::BrowserScanResult, String> {
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<String>) -> Result<cleaner::CleanResult, String> {
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<backend::models::BrowserScanResult, String> {
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<String>,
) -> Result<backend::models::CleanResult, String> {
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<u64, String> {
cleaner::run_memory_clean().await
backend::memory_clean::run_memory_clean().await
}
#[tauri::command]
async fn run_deep_memory_clean() -> Result<u64, String> {
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,