use crate::ui::Theme;
use anyhow::{Context, Result, anyhow};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct FileInfo {
pub path: PathBuf,
pub is_dir: bool,
pub size: u64,
pub is_symlink: bool,
}
pub fn validate_paths(paths: &[PathBuf]) -> Result<()> {
for path in paths {
if !path.exists() {
return Err(anyhow!("路径不存在: {}", path.display()));
}
}
Ok(())
}
pub fn collect_files_to_remove(paths: &[PathBuf], recursive: bool) -> Result<Vec<FileInfo>> {
let mut files = Vec::new();
for path in paths {
let metadata = path
.symlink_metadata()
.with_context(|| format!("无法获取文件元数据: {}", path.display()))?;
let is_symlink = metadata.file_type().is_symlink();
let is_dir = metadata.is_dir() && !is_symlink;
if is_dir {
if recursive {
collect_dir_files(path, &mut files)?;
files.push(FileInfo {
path: path.clone(),
is_dir: true,
size: 0,
is_symlink: false,
});
} else {
if path.read_dir()?.next().is_some() {
return Err(anyhow!(
"目录删除需要 -r/--recursive 参数: {}",
path.display()
));
}
files.push(FileInfo {
path: path.clone(),
is_dir: true,
size: 0,
is_symlink: false,
});
}
} else {
files.push(FileInfo {
path: path.clone(),
is_dir: false,
size: metadata.len(),
is_symlink,
});
}
}
Ok(files)
}
fn collect_dir_files(dir: &Path, files: &mut Vec<FileInfo>) -> Result<()> {
for entry in fs::read_dir(dir).with_context(|| format!("无法读取目录: {}", dir.display()))?
{
let entry = entry.with_context(|| format!("无法读取目录项: {}", dir.display()))?;
let path = entry.path();
let metadata = path
.symlink_metadata()
.with_context(|| format!("无法获取文件元数据: {}", path.display()))?;
let is_symlink = metadata.file_type().is_symlink();
let is_dir = metadata.is_dir() && !is_symlink;
if is_dir {
collect_dir_files(&path, files)?;
files.push(FileInfo {
path,
is_dir: true,
size: 0,
is_symlink: false,
});
} else {
files.push(FileInfo {
path,
is_dir: false,
size: metadata.len(),
is_symlink,
});
}
}
Ok(())
}
pub fn remove_files(
files: &[FileInfo],
dry_run: bool,
verbose: bool,
_anyway: bool,
) -> Vec<(PathBuf, Result<()>)> {
let theme = Theme::new();
let mut results = Vec::new();
#[cfg(target_os = "windows")]
{
if let Some(root_dir) = files.iter().find(|f| {
f.is_dir
&& !files
.iter()
.any(|other| other.path != f.path && f.path.starts_with(&other.path))
}) {
if !dry_run {
match remove_dir_all_with_symlinks(&root_dir.path) {
Ok(_) => {
if verbose {
println!(
"{} {}",
theme.icon_success(),
theme.muted(format!("删除 {}", root_dir.path.display()))
);
}
results.push((root_dir.path.clone(), Ok(())));
return results;
}
Err(e) => {
if verbose {
println!(
"{} {}",
theme.icon_warning(),
theme.warning(format!("批量删除失败,尝试逐个删除: {}", e))
);
}
}
}
} else {
results.push((root_dir.path.clone(), Ok(())));
return results;
}
}
}
let mut sorted = files.to_vec();
sorted.sort_by(|a, b| {
if a.is_dir && !b.is_dir {
std::cmp::Ordering::Greater
} else if !a.is_dir && b.is_dir {
std::cmp::Ordering::Less
} else {
let depth_a = a.path.components().count();
let depth_b = b.path.components().count();
depth_b.cmp(&depth_a)
}
});
for file in sorted {
let result = if dry_run {
Ok(())
} else {
remove_entry(&file).with_context(|| format!("删除失败: {}", file.path.display()))
};
if verbose {
match &result {
Ok(_) => println!(
"{} {}",
theme.icon_success(),
theme.muted(format!("删除 {}", file.path.display()))
),
Err(e) => println!(
"{} {}",
theme.icon_error(),
theme.error(format!("删除失败 {} - {}", file.path.display(), e))
),
}
}
results.push((file.path, result));
}
results
}
#[cfg(target_os = "windows")]
fn remove_dir_all_with_symlinks(path: &Path) -> Result<()> {
if let Err(_) = remove_readonly_recursively(path) {
}
fs::remove_dir_all(path).with_context(|| format!("无法删除目录: {}", path.display()))
}
#[cfg(target_os = "windows")]
fn remove_readonly_recursively(path: &Path) -> Result<()> {
let metadata = path
.symlink_metadata()
.with_context(|| format!("无法获取路径元数据: {}", path.display()))?;
if !metadata.file_type().is_symlink() {
#[allow(clippy::permissions_set_readonly_false)]
{
let mut perms = metadata.permissions();
perms.set_readonly(false);
fs::set_permissions(path, perms)
.with_context(|| format!("无法设置权限: {}", path.display()))?;
}
if metadata.is_dir() {
for entry in
fs::read_dir(path).with_context(|| format!("无法读取目录: {}", path.display()))?
{
let entry = entry.with_context(|| format!("无法读取目录项: {}", path.display()))?;
remove_readonly_recursively(&entry.path())?;
}
}
}
Ok(())
}
fn remove_entry(file: &FileInfo) -> Result<()> {
#[cfg(target_os = "windows")]
{
if file.is_symlink {
match fs::remove_file(&file.path) {
Ok(_) => return Ok(()),
Err(e) => {
if e.kind() == std::io::ErrorKind::PermissionDenied {
if let Ok(metadata) = file.path.metadata() {
#[allow(clippy::permissions_set_readonly_false)]
{
let mut attrs = metadata.permissions();
attrs.set_readonly(false);
if let Err(_) = fs::set_permissions(&file.path, attrs) {
}
}
}
return fs::remove_file(&file.path)
.with_context(|| format!("无法删除符号链接: {}", file.path.display()));
}
return Err(e.into());
}
}
}
}
let result = if file.is_symlink {
fs::remove_file(&file.path)
} else if file.is_dir {
match fs::remove_dir(&file.path) {
Ok(_) => Ok(()),
Err(e) => {
if e.kind() == std::io::ErrorKind::PermissionDenied {
#[cfg(target_os = "windows")]
if let Err(_) = remove_readonly_recursively(&file.path) {
}
fs::remove_dir_all(&file.path)
} else {
Err(e)
}
}
}
} else {
fs::remove_file(&file.path)
};
result.with_context(|| format!("删除失败: {}", file.path.display()))
}
pub fn format_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = size as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", size as u64, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}