use std::fs;
use std::path::PathBuf;
use tokio::sync::mpsc;
use super::constants::SCAN_CHANNEL_SIZE;
use super::state::{DeletionProgress, ScanResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DeletionMode {
#[default]
Trash,
#[allow(dead_code)]
Permanent,
}
pub fn start_deletion(items: Vec<(PathBuf, u64)>) -> mpsc::Receiver<ScanResult> {
start_deletion_with_mode(items, DeletionMode::Trash)
}
pub fn start_deletion_with_mode(
items: Vec<(PathBuf, u64)>,
mode: DeletionMode,
) -> mpsc::Receiver<ScanResult> {
let (tx, rx) = mpsc::channel(SCAN_CHANNEL_SIZE);
let total = items.len();
if total == 0 {
return rx;
}
tokio::spawn(async move {
let mut deleted = 0;
let mut failed = 0;
let mut bytes_freed: u64 = 0;
for (i, (path, size)) in items.iter().enumerate() {
let _ = tx
.send(ScanResult::DeletionProgress(DeletionProgress {
total,
deleted,
failed,
bytes_freed,
current: Some(path.clone()),
}))
.await;
let path_clone = path.clone();
let result = tokio::task::spawn_blocking(move || delete_path(&path_clone, mode)).await;
match result {
Ok(Ok(())) => {
deleted += 1;
bytes_freed += size;
}
Ok(Err(e)) => {
tracing::warn!("Failed to delete {:?}: {}", path, e);
failed += 1;
}
Err(e) => {
tracing::warn!("Deletion task panicked for {:?}: {}", path, e);
failed += 1;
}
}
if i < total - 1 {
let _ = tx
.send(ScanResult::DeletionProgress(DeletionProgress {
total,
deleted,
failed,
bytes_freed,
current: items.get(i + 1).map(|(p, _)| p.clone()),
}))
.await;
}
}
let _ = tx
.send(ScanResult::DeletionComplete {
deleted,
failed,
bytes_freed,
})
.await;
});
rx
}
fn delete_path(path: &PathBuf, mode: DeletionMode) -> Result<(), String> {
let metadata = match fs::symlink_metadata(path) {
Ok(m) => m,
Err(e) => return Err(format!("Cannot access path: {}", e)),
};
match mode {
DeletionMode::Trash => {
trash::delete(path).map_err(|e| format!("Failed to move to trash: {}", e))
}
DeletionMode::Permanent => {
if metadata.is_symlink() {
fs::remove_file(path).map_err(|e| format!("Failed to remove symlink: {}", e))
} else if metadata.is_dir() {
fs::remove_dir_all(path).map_err(|e| format!("Failed to remove directory: {}", e))
} else {
fs::remove_file(path).map_err(|e| format!("Failed to remove file: {}", e))
}
}
}
}
pub fn format_deletion_result(deleted: usize, failed: usize, bytes_freed: u64) -> (bool, String) {
use crate::ui::format_size;
let success = failed == 0;
let msg = if success {
format!(
"Moved {} items to trash, freed {}",
deleted,
format_size(bytes_freed)
)
} else if deleted > 0 {
format!(
"Moved {} to trash, {} failed (freed {})",
deleted,
failed,
format_size(bytes_freed)
)
} else {
format!("Failed to delete {} items", failed)
};
(success, msg)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_delete_regular_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
File::create(&file_path).unwrap();
assert!(file_path.exists());
delete_path(&file_path, DeletionMode::Permanent).unwrap();
assert!(!file_path.exists());
}
#[test]
fn test_delete_directory() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().join("subdir");
fs::create_dir(&dir_path).unwrap();
File::create(dir_path.join("file.txt")).unwrap();
assert!(dir_path.exists());
delete_path(&dir_path, DeletionMode::Permanent).unwrap();
assert!(!dir_path.exists());
}
#[test]
fn test_delete_nested_directory() {
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().join("level1");
fs::create_dir(&dir_path).unwrap();
fs::create_dir(dir_path.join("level2")).unwrap();
fs::create_dir(dir_path.join("level2/level3")).unwrap();
File::create(dir_path.join("level2/level3/deep_file.txt")).unwrap();
assert!(dir_path.exists());
delete_path(&dir_path, DeletionMode::Permanent).unwrap();
assert!(!dir_path.exists());
}
#[test]
fn test_delete_nonexistent_path() {
let temp_dir = TempDir::new().unwrap();
let nonexistent = temp_dir.path().join("does_not_exist.txt");
let result = delete_path(&nonexistent, DeletionMode::Permanent);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Cannot access path"));
}
#[test]
fn test_delete_empty_directory() {
let temp_dir = TempDir::new().unwrap();
let empty_dir = temp_dir.path().join("empty");
fs::create_dir(&empty_dir).unwrap();
assert!(empty_dir.exists());
delete_path(&empty_dir, DeletionMode::Permanent).unwrap();
assert!(!empty_dir.exists());
}
#[test]
fn test_delete_file_with_content() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("content.txt");
let mut file = File::create(&file_path).unwrap();
writeln!(file, "Some content that should be deleted").unwrap();
drop(file);
assert!(file_path.exists());
delete_path(&file_path, DeletionMode::Permanent).unwrap();
assert!(!file_path.exists());
}
#[cfg(unix)]
#[test]
fn test_delete_symlink_to_file() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("target.txt");
let link_path = temp_dir.path().join("link.txt");
File::create(&file_path).unwrap();
symlink(&file_path, &link_path).unwrap();
assert!(file_path.exists());
assert!(link_path.symlink_metadata().unwrap().is_symlink());
delete_path(&link_path, DeletionMode::Permanent).unwrap();
assert!(!link_path.exists());
assert!(
file_path.exists(),
"Target file should NOT be deleted when deleting symlink"
);
}
#[cfg(unix)]
#[test]
fn test_delete_symlink_to_directory() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let dir_path = temp_dir.path().join("target_dir");
let link_path = temp_dir.path().join("link_dir");
fs::create_dir(&dir_path).unwrap();
File::create(dir_path.join("file.txt")).unwrap();
symlink(&dir_path, &link_path).unwrap();
assert!(dir_path.exists());
assert!(link_path.symlink_metadata().unwrap().is_symlink());
delete_path(&link_path, DeletionMode::Permanent).unwrap();
assert!(!link_path.exists());
assert!(
dir_path.exists(),
"Target directory should NOT be deleted when deleting symlink"
);
assert!(
dir_path.join("file.txt").exists(),
"Files in target directory should NOT be deleted"
);
}
#[cfg(unix)]
#[test]
fn test_delete_broken_symlink() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let link_path = temp_dir.path().join("broken_link");
symlink("/nonexistent/path/that/does/not/exist", &link_path).unwrap();
assert!(link_path.symlink_metadata().unwrap().is_symlink());
assert!(!link_path.exists());
delete_path(&link_path, DeletionMode::Permanent).unwrap();
assert!(link_path.symlink_metadata().is_err());
}
#[cfg(unix)]
#[test]
fn test_delete_symlink_chain() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("target.txt");
let link1_path = temp_dir.path().join("link1");
let link2_path = temp_dir.path().join("link2");
File::create(&file_path).unwrap();
symlink(&file_path, &link1_path).unwrap();
symlink(&link1_path, &link2_path).unwrap();
delete_path(&link1_path, DeletionMode::Permanent).unwrap();
assert!(link1_path.symlink_metadata().is_err());
assert!(file_path.exists());
assert!(link2_path.symlink_metadata().unwrap().is_symlink());
assert!(!link2_path.exists()); }
#[cfg(unix)]
#[test]
fn test_delete_directory_containing_symlinks() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let outside_file = temp_dir.path().join("outside.txt");
let dir_path = temp_dir.path().join("container");
let link_in_dir = dir_path.join("link_to_outside");
File::create(&outside_file).unwrap();
fs::create_dir(&dir_path).unwrap();
symlink(&outside_file, &link_in_dir).unwrap();
delete_path(&dir_path, DeletionMode::Permanent).unwrap();
assert!(!dir_path.exists());
assert!(
outside_file.exists(),
"File outside directory should NOT be deleted"
);
}
#[test]
fn test_format_deletion_result_success() {
let (success, msg) = format_deletion_result(5, 0, 1024 * 1024);
assert!(success);
assert!(msg.contains("5 items"));
assert!(msg.contains("trash"));
}
#[test]
fn test_format_deletion_result_partial_failure() {
let (success, msg) = format_deletion_result(3, 2, 512 * 1024);
assert!(!success);
assert!(msg.contains("3"));
assert!(msg.contains("2 failed"));
}
#[test]
fn test_format_deletion_result_total_failure() {
let (success, msg) = format_deletion_result(0, 5, 0);
assert!(!success);
assert!(msg.contains("Failed"));
assert!(msg.contains("5"));
}
#[test]
fn test_deletion_mode_default() {
let mode = DeletionMode::default();
assert_eq!(mode, DeletionMode::Trash);
}
}