Files
win-cleaner/src-tauri/src/cleaner.rs
2026-03-03 20:01:50 -04:00

495 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})
}