495 lines
16 KiB
Rust
495 lines
16 KiB
Rust
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,
|
||
})
|
||
}
|