feat: add batch_delete_notes and empty_trash Rust commands

Add two new Tauri commands for trash management:
- batch_delete_notes: permanently delete multiple note files from disk
- empty_trash: scan vault and delete ALL trashed notes regardless of age

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-12 00:00:34 +01:00
parent 9994f2386c
commit 13d3b2d375
4 changed files with 137 additions and 1 deletions

View File

@@ -114,6 +114,18 @@ pub fn delete_note(path: String) -> Result<String, String> {
vault::delete_note(&path)
}
#[tauri::command]
pub fn batch_delete_notes(paths: Vec<String>) -> Result<Vec<String>, String> {
let expanded: Vec<String> = paths.iter().map(|p| expand_tilde(p).into_owned()).collect();
vault::batch_delete_notes(&expanded)
}
#[tauri::command]
pub fn empty_trash(vault_path: String) -> Result<Vec<String>, String> {
let vault_path = expand_tilde(&vault_path);
vault::empty_trash(&vault_path)
}
#[tauri::command]
pub fn migrate_is_a_to_type(vault_path: String) -> Result<usize, String> {
let vault_path = expand_tilde(&vault_path);

View File

@@ -143,6 +143,8 @@ pub fn run() {
commands::copy_image_to_vault,
commands::purge_trash,
commands::delete_note,
commands::batch_delete_notes,
commands::empty_trash,
commands::migrate_is_a_to_type,
commands::batch_archive_notes,
commands::batch_trash_notes,

View File

@@ -13,7 +13,7 @@ pub use getting_started::{create_getting_started_vault, default_vault_path, vaul
pub use image::{copy_image_to_vault, save_image};
pub use migration::migrate_is_a_to_type;
pub use rename::{move_note_to_type_folder, rename_note, MoveResult, RenameResult};
pub use trash::{delete_note, is_file_trashed, purge_trash};
pub use trash::{batch_delete_notes, delete_note, empty_trash, is_file_trashed, purge_trash};
use parsing::{
contains_wikilink, count_body_words, extract_outgoing_links, extract_snippet, extract_title,

View File

@@ -88,6 +88,47 @@ pub fn is_file_trashed(path: &Path) -> bool {
false
}
/// Delete multiple note files from disk.
/// Returns the list of successfully deleted paths.
/// Skips files that don't exist or fail to delete (logs warnings).
pub fn batch_delete_notes(paths: &[String]) -> Result<Vec<String>, String> {
let mut deleted = Vec::new();
for path in paths {
let file = Path::new(path.as_str());
match try_purge_file(file) {
Some(p) => deleted.push(p),
None if !file.exists() => {
log::warn!("File does not exist, skipping: {}", path);
}
None => {} // try_purge_file already logged the warning
}
}
Ok(deleted)
}
/// Scan all markdown files in the vault and delete ALL trashed notes
/// (regardless of age). Returns the list of deleted file paths.
pub fn empty_trash(vault_path: &str) -> Result<Vec<String>, String> {
let vault = Path::new(vault_path);
if !vault.exists() || !vault.is_dir() {
return Err(format!(
"Vault path does not exist or is not a directory: {}",
vault_path
));
}
let deleted: Vec<String> = WalkDir::new(vault)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| is_markdown_file(e.path()))
.filter(|e| is_file_trashed(e.path()))
.filter_map(|entry| try_purge_file(entry.path()))
.collect();
Ok(deleted)
}
/// Scan all markdown files in the vault and delete those where
/// `Trashed at` frontmatter is more than 30 days ago.
/// Returns the list of deleted file paths.
@@ -342,4 +383,85 @@ mod tests {
);
assert!(!is_file_trashed(&dir.path().join("active.md")));
}
#[test]
fn test_batch_delete_notes_removes_files() {
let dir = TempDir::new().unwrap();
create_test_file(dir.path(), "a.md", "---\ntitle: A\n---\n# A\n");
create_test_file(dir.path(), "b.md", "---\ntitle: B\n---\n# B\n");
create_test_file(dir.path(), "keep.md", "---\ntitle: Keep\n---\n# Keep\n");
let paths = vec![
dir.path().join("a.md").to_str().unwrap().to_string(),
dir.path().join("b.md").to_str().unwrap().to_string(),
];
let deleted = batch_delete_notes(&paths).unwrap();
assert_eq!(deleted.len(), 2);
assert!(!dir.path().join("a.md").exists());
assert!(!dir.path().join("b.md").exists());
assert!(dir.path().join("keep.md").exists());
}
#[test]
fn test_batch_delete_notes_skips_nonexistent() {
let dir = TempDir::new().unwrap();
create_test_file(dir.path(), "exists.md", "---\ntitle: X\n---\n# X\n");
let paths = vec![
dir.path().join("exists.md").to_str().unwrap().to_string(),
"/nonexistent/path.md".to_string(),
];
let deleted = batch_delete_notes(&paths).unwrap();
assert_eq!(deleted.len(), 1);
assert!(!dir.path().join("exists.md").exists());
}
#[test]
fn test_empty_trash_deletes_all_trashed() {
let dir = TempDir::new().unwrap();
// Recently trashed — should be deleted
let recent = chrono::Utc::now()
.date_naive()
.format("%Y-%m-%d")
.to_string();
create_test_file(
dir.path(),
"recent.md",
&format!(
"---\nTrashed: true\nTrashed at: \"{}\"\n---\n# Recent\n",
recent
),
);
// Old trashed — should be deleted
create_test_file(
dir.path(),
"old.md",
"---\nTrashed: true\nTrashed at: \"2025-01-01\"\n---\n# Old\n",
);
// Not trashed — should be kept
create_test_file(
dir.path(),
"normal.md",
"---\ntype: Note\n---\n# Normal\n",
);
let deleted = empty_trash(dir.path().to_str().unwrap()).unwrap();
assert_eq!(deleted.len(), 2);
assert!(!dir.path().join("recent.md").exists());
assert!(!dir.path().join("old.md").exists());
assert!(dir.path().join("normal.md").exists());
}
#[test]
fn test_empty_trash_empty_vault() {
let dir = TempDir::new().unwrap();
let deleted = empty_trash(dir.path().to_str().unwrap()).unwrap();
assert!(deleted.is_empty());
}
#[test]
fn test_empty_trash_nonexistent_path() {
let result = empty_trash("/nonexistent/path/that/does/not/exist");
assert!(result.is_err());
}
}