From 69bd1ce36d49fb52c18493db629f6e724396be6f Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 5 Feb 2026 19:58:23 +0000 Subject: [PATCH 1/6] chore: add restore_all support to macos Co-authored-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com> --- src/macos/mod.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 7167d9a..999c9f8 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -287,5 +287,21 @@ fn esc_quote(s: &str) -> Cow<'_, str> { } } +pub fn restore_all(items: I) -> Result<(), Error> +where + I: IntoIterator, +{ + let mut iter = items.into_iter(); + while let Some(item) = iter.next() { + let original_path = item.original_path(); + let trash_path = Path::new(&item.id); + + std::fs::create_dir_all(&item.original_parent).map_err(|error| into_unknown(error.to_string()))?; + std::fs::rename(&trash_path, &original_path).map_err(|error| into_unknown(error.to_string()))?; + } + + Ok(()) +} + #[cfg(test)] mod tests; From d92578e94ab88fa49d9082b198e234c320393bb6 Mon Sep 17 00:00:00 2001 From: dino Date: Wed, 25 Feb 2026 23:02:22 +0000 Subject: [PATCH 2/6] feat(macos): finish restore_all implementation * Move `trash::restore_all` from the `os_limited` module to its parent module, as we want this to be available also to macOS, but not want to be bothered, at least for now, with implementing all of the `os_limited` module's functionality * Update macOS' `restore_all` implementation to correctly handle restore collisions in case a file in the trashed item's location already exists * Add new tests to ensure that restore collisions are handled correctly Co-authored-by: Agus Zubiaga --- src/lib.rs | 156 +++++++++++++++++++++++---------------------- src/macos/mod.rs | 19 +++++- src/macos/tests.rs | 57 +++++++++++++++++ tests/trash.rs | 3 - 4 files changed, 154 insertions(+), 81 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9f950c1..74fe447 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,10 +186,8 @@ pub enum Error { description: String, }, - /// **freedesktop only** - /// /// Error coming from file system - #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))] + #[cfg(all(unix, not(target_os = "ios"), not(target_os = "android")))] FileSystem { path: PathBuf, source: std::io::Error, @@ -268,6 +266,11 @@ pub fn into_unknown(err: E) -> Error { Error::Unknown { description: format!("{err}") } } +#[cfg(all(unix, not(target_os = "ios"), not(target_os = "android")))] +pub fn fs_error(path: impl Into, source: std::io::Error) -> Error { + Error::FileSystem { path: path.into(), source } +} + pub(crate) fn canonicalize_paths(paths: I) -> Result, Error> where I: IntoIterator, @@ -386,6 +389,81 @@ pub struct TrashItemMetadata { pub size: TrashItemSize, } +#[cfg(all(not(target_os = "ios"), not(target_os = "android")))] +/// Restores all the provided [`TrashItem`] to their original location. +/// +/// This function consumes the provided items. +/// +/// # Errors +/// +/// Errors this function may return include but are not limited to the following. +/// +/// It may be the case that when restoring a file or a folder, the `original_path` already has +/// a new item with the same name. When such a collision happens this function returns a +/// [`RestoreCollision`] kind of error. +/// +/// If two or more of the provided items have identical `original_path`s then a +/// [`RestoreTwins`] kind of error is returned. +/// +/// # Example +/// +/// Basic usage: +/// +/// ``` +/// use std::fs::File; +/// use trash::os_limited::{list, restore_all}; +/// +/// let filename = "trash-restore_all-example"; +/// File::create_new(filename).unwrap(); +/// restore_all(list().unwrap().into_iter().filter(|x| x.name == filename)).unwrap(); +/// std::fs::remove_file(filename).unwrap(); +/// ``` +/// +/// Retry restoring when encountering [`RestoreCollision`] error: +/// +/// ```no_run +/// use trash::os_limited::{list, restore_all}; +/// use trash::Error::RestoreCollision; +/// +/// let items = list().unwrap(); +/// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items) { +/// // keep all except the one(s) that couldn't be restored +/// remaining_items.retain(|e| e.original_path() != path); +/// restore_all(remaining_items).unwrap(); +/// } +/// ``` +/// +/// [`RestoreCollision`]: Error::RestoreCollision +/// [`RestoreTwins`]: Error::RestoreTwins +pub fn restore_all(items: I) -> Result<(), Error> +where + I: IntoIterator, +{ + use std::collections::HashSet; + + // Check for twins here cause that's pretty platform independent. + struct ItemWrapper<'a>(&'a TrashItem); + impl PartialEq for ItemWrapper<'_> { + fn eq(&self, other: &Self) -> bool { + self.0.original_path() == other.0.original_path() + } + } + impl Eq for ItemWrapper<'_> {} + impl Hash for ItemWrapper<'_> { + fn hash(&self, state: &mut H) { + self.0.original_path().hash(state); + } + } + let items = items.into_iter().collect::>(); + let mut item_set = HashSet::with_capacity(items.len()); + for item in items.iter() { + if !item_set.insert(ItemWrapper(item)) { + return Err(Error::RestoreTwins { path: item.original_path(), items }); + } + } + platform::restore_all(items) +} + #[cfg(any( target_os = "windows", all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")) @@ -513,76 +591,4 @@ pub mod os_limited { { platform::purge_all(items) } - - /// Restores all the provided [`TrashItem`] to their original location. - /// - /// This function consumes the provided items. - /// - /// # Errors - /// - /// Errors this function may return include but are not limited to the following. - /// - /// It may be the case that when restoring a file or a folder, the `original_path` already has - /// a new item with the same name. When such a collision happens this function returns a - /// [`RestoreCollision`] kind of error. - /// - /// If two or more of the provided items have identical `original_path`s then a - /// [`RestoreTwins`] kind of error is returned. - /// - /// # Example - /// - /// Basic usage: - /// - /// ``` - /// use std::fs::File; - /// use trash::os_limited::{list, restore_all}; - /// - /// let filename = "trash-restore_all-example"; - /// File::create_new(filename).unwrap(); - /// restore_all(list().unwrap().into_iter().filter(|x| x.name == filename)).unwrap(); - /// std::fs::remove_file(filename).unwrap(); - /// ``` - /// - /// Retry restoring when encountering [`RestoreCollision`] error: - /// - /// ```no_run - /// use trash::os_limited::{list, restore_all}; - /// use trash::Error::RestoreCollision; - /// - /// let items = list().unwrap(); - /// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items) { - /// // keep all except the one(s) that couldn't be restored - /// remaining_items.retain(|e| e.original_path() != path); - /// restore_all(remaining_items).unwrap(); - /// } - /// ``` - /// - /// [`RestoreCollision`]: Error::RestoreCollision - /// [`RestoreTwins`]: Error::RestoreTwins - pub fn restore_all(items: I) -> Result<(), Error> - where - I: IntoIterator, - { - // Check for twins here cause that's pretty platform independent. - struct ItemWrapper<'a>(&'a TrashItem); - impl PartialEq for ItemWrapper<'_> { - fn eq(&self, other: &Self) -> bool { - self.0.original_path() == other.0.original_path() - } - } - impl Eq for ItemWrapper<'_> {} - impl Hash for ItemWrapper<'_> { - fn hash(&self, state: &mut H) { - self.0.original_path().hash(state); - } - } - let items = items.into_iter().collect::>(); - let mut item_set = HashSet::with_capacity(items.len()); - for item in items.iter() { - if !item_set.insert(ItemWrapper(item)) { - return Err(Error::RestoreTwins { path: item.original_path(), items }); - } - } - platform::restore_all(items) - } } diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 999c9f8..7d1e0df 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -8,7 +8,7 @@ use std::{ use log::trace; use objc2_foundation::{NSFileManager, NSString, NSURL}; -use crate::{into_unknown, Error, TrashContext, TrashItem}; +use crate::{fs_error, into_unknown, Error, TrashContext, TrashItem}; #[derive(Copy, Clone, Debug)] /// There are 2 ways to trash files: via the ≝Finder app or via the OS NsFileManager call @@ -287,6 +287,11 @@ fn esc_quote(s: &str) -> Cow<'_, str> { } } +/// Does a basic restore using file renaming, ignoring whether the +/// `DeleteMethod::NSFileManager` or `DeleteMethod::Finder` was used when +/// deleting the file, which means that files deleted with +/// `DeleteMethod::Finder` will not correctly update the `.DS_Store` file that +/// is kept in macOS' trash. pub fn restore_all(items: I) -> Result<(), Error> where I: IntoIterator, @@ -296,8 +301,16 @@ where let original_path = item.original_path(); let trash_path = Path::new(&item.id); - std::fs::create_dir_all(&item.original_parent).map_err(|error| into_unknown(error.to_string()))?; - std::fs::rename(&trash_path, &original_path).map_err(|error| into_unknown(error.to_string()))?; + std::fs::create_dir_all(&item.original_parent).map_err(|error| fs_error(&original_path, error))?; + + if std::fs::exists(&original_path).map_err(|error| fs_error(&original_path, error))? { + return Err(Error::RestoreCollision { + path: original_path, + remaining_items: std::iter::once(item).chain(iter).collect::>(), + }); + } + + std::fs::rename(&trash_path, &original_path).map_err(|error| fs_error(&original_path, error))?; } Ok(()) diff --git a/src/macos/tests.rs b/src/macos/tests.rs index 21ec52c..be07395 100644 --- a/src/macos/tests.rs +++ b/src/macos/tests.rs @@ -1,5 +1,6 @@ use crate::{ macos::{percent_encode, DeleteMethod, TrashContextExtMacos}, + restore_all, tests::{get_unique_name, init_logging}, TrashContext, }; @@ -174,3 +175,59 @@ fn test_delete_with_info_finder() { _ => panic!("Calling delete_with_info with Finder method failed to return TrashItem."), } } + +#[test] +#[serial] +fn test_restore_all_restore_collision_file_manager() { + let mut cleanup_paths = CleanupPaths::new(); + let path = std::env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); + cleanup_paths.push(path.clone()); + File::create_new(&path).unwrap(); + + let mut trash = TrashContext::new(); + trash.set_delete_method(DeleteMethod::NsFileManager); + + let trash_item = trash.delete_with_info(&path).expect("Should be able to delete file"); + cleanup_paths.push(PathBuf::from(&trash_item.id)); + + // Create a new file where the original trashed item was, so that restoring + // it causes a collision. + File::create_new(&path).expect("Should be able to create file for collision"); + + match restore_all(vec![trash_item.clone()]) { + Err(super::Error::RestoreCollision { path: collision_path, remaining_items }) => { + assert_eq!(collision_path, path); + assert_eq!(remaining_items.len(), 1); + assert_eq!(remaining_items[0].original_path(), path); + } + _ => panic!("Calling delete_with_info with Finder method failed to return TrashItem."), + }; +} + +#[test] +#[serial] +fn test_restore_all_restore_collision_finder() { + let mut cleanup_paths = CleanupPaths::new(); + let path = std::env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); + cleanup_paths.push(path.clone()); + File::create_new(&path).unwrap(); + + let mut trash = TrashContext::new(); + trash.set_delete_method(DeleteMethod::Finder); + + let trash_item = trash.delete_with_info(&path).expect("Should be able to delete file"); + cleanup_paths.push(PathBuf::from(&trash_item.id)); + + // Create a new file where the original trashed item was, so that restoring + // it causes a collision. + File::create_new(&path).expect("Should be able to create file for collision"); + + match restore_all(vec![trash_item.clone()]) { + Err(super::Error::RestoreCollision { path: collision_path, remaining_items }) => { + assert_eq!(collision_path, path); + assert_eq!(remaining_items.len(), 1); + assert_eq!(remaining_items[0].original_path(), path); + } + _ => panic!("Calling delete_with_info with Finder method failed to return TrashItem."), + }; +} diff --git a/tests/trash.rs b/tests/trash.rs index 378d218..68e7661 100644 --- a/tests/trash.rs +++ b/tests/trash.rs @@ -6,9 +6,6 @@ use log::trace; use serial_test::serial; use trash::{delete, delete_all}; -use std::env; -use trash::TrashContext; - mod util { use std::sync::atomic::{AtomicI32, Ordering}; From e301438a99623a1a8950d478bc408e02fd58fc88 Mon Sep 17 00:00:00 2001 From: dino Date: Thu, 26 Feb 2026 09:33:51 +0000 Subject: [PATCH 3/6] fix: fix failing build --- src/freedesktop.rs | 3 ++- src/lib.rs | 15 ++++++--------- src/macos/mod.rs | 2 +- src/tests.rs | 8 ++++---- tests/trash.rs | 3 ++- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index 2920005..f86a864 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -952,8 +952,9 @@ mod tests { use crate::{ canonicalize_paths, delete, delete_all, - os_limited::{list, purge_all, restore_all}, + os_limited::{list, purge_all}, platform::encode_uri_path, + restore_all, tests::get_unique_name, Error, }; diff --git a/src/lib.rs b/src/lib.rs index 74fe447..3d244c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -411,21 +411,22 @@ pub struct TrashItemMetadata { /// /// ``` /// use std::fs::File; -/// use trash::os_limited::{list, restore_all}; +/// use trash::{delete_with_info, restore_all}; /// /// let filename = "trash-restore_all-example"; /// File::create_new(filename).unwrap(); -/// restore_all(list().unwrap().into_iter().filter(|x| x.name == filename)).unwrap(); +/// let item = delete_with_info(filename).unwrap(); +/// restore_all([item]).unwrap(); /// std::fs::remove_file(filename).unwrap(); /// ``` /// /// Retry restoring when encountering [`RestoreCollision`] error: /// /// ```no_run -/// use trash::os_limited::{list, restore_all}; +/// use trash::{delete_with_info, restore_all}; /// use trash::Error::RestoreCollision; /// -/// let items = list().unwrap(); +/// let items = vec![delete_with_info("some-file").unwrap()]; /// if let Err(RestoreCollision { path, mut remaining_items }) = restore_all(items) { /// // keep all except the one(s) that couldn't be restored /// remaining_items.retain(|e| e.original_path() != path); @@ -472,11 +473,7 @@ pub mod os_limited { //! This module provides functionality which is only supported on Windows and //! Linux or other Freedesktop Trash compliant environment. - use std::{ - borrow::Borrow, - collections::HashSet, - hash::{Hash, Hasher}, - }; + use std::{borrow::Borrow, collections::HashSet}; use super::{platform, Error, TrashItem, TrashItemMetadata}; diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 7d1e0df..8273a65 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -310,7 +310,7 @@ where }); } - std::fs::rename(&trash_path, &original_path).map_err(|error| fs_error(&original_path, error))?; + std::fs::rename(trash_path, &original_path).map_err(|error| fs_error(&original_path, error))?; } Ok(()) diff --git a/src/tests.rs b/src/tests.rs index 41ad08a..5143fba 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -123,7 +123,7 @@ mod os_limited { #[test] fn restore_empty() { init_logging(); - trash::os_limited::restore_all(vec![]).unwrap(); + trash::restore_all(vec![]).unwrap(); } #[test] @@ -176,7 +176,7 @@ mod os_limited { .filter(|x| x.name.as_encoded_bytes().starts_with(file_name_prefix.as_bytes())) .collect(); assert_eq!(targets.len(), file_count); - trash::os_limited::restore_all(targets).unwrap(); + trash::restore_all(targets).unwrap(); let remaining = trash::os_limited::list() .unwrap() .into_iter() @@ -220,7 +220,7 @@ mod os_limited { .collect(); targets.sort_by(|a, b| a.name.cmp(&b.name)); assert_eq!(targets.len(), file_count); - let remaining_count = match trash::os_limited::restore_all(targets) { + let remaining_count = match trash::restore_all(targets) { Err(trash::Error::RestoreCollision { remaining_items, .. }) => { let contains = |v: &Vec, name: &String| { for curr in v.iter() { @@ -279,7 +279,7 @@ mod os_limited { .collect(); targets.sort_by(|a, b| a.name.cmp(&b.name)); assert_eq!(targets.len(), file_count + 1); // plus one for one of the twins - match trash::os_limited::restore_all(targets) { + match trash::restore_all(targets) { Err(trash::Error::RestoreTwins { path, items }) => { assert_eq!(path.file_name().unwrap().to_str().unwrap(), twin_name); trash::os_limited::purge_all(items).unwrap(); diff --git a/tests/trash.rs b/tests/trash.rs index 68e7661..6db62da 100644 --- a/tests/trash.rs +++ b/tests/trash.rs @@ -1,10 +1,11 @@ +use std::env; use std::fs::{create_dir, File}; use std::path::{Path, PathBuf}; use log::trace; use serial_test::serial; -use trash::{delete, delete_all}; +use trash::{delete, delete_all, TrashContext}; mod util { use std::sync::atomic::{AtomicI32, Ordering}; From c3e7cfca3e6470a17c8f8ba5fc08e07e261d44b7 Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 27 Feb 2026 15:10:11 +0000 Subject: [PATCH 4/6] fix(windows): fix clippy warnings on windows --- src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3d244c2..d9f0679 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -473,7 +473,10 @@ pub mod os_limited { //! This module provides functionality which is only supported on Windows and //! Linux or other Freedesktop Trash compliant environment. - use std::{borrow::Borrow, collections::HashSet}; + use std::borrow::Borrow; + + #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))] + use std::collections::HashSet; use super::{platform, Error, TrashItem, TrashItemMetadata}; From aeda034028d5f7246d6aad59233ace97f42f3101 Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 27 Feb 2026 15:28:26 +0000 Subject: [PATCH 5/6] chore(macos): ensure that trash item exists before restoring The `restore_all` implementation on macOS only checked if there was a file collision when attempting to restore a file. This commit updates the implementation so as to also confirm that the provided trash item's file does exist before attempting to create the original parent folder as well as moving the file. --- src/macos/mod.rs | 7 ++++++- src/macos/tests.rs | 24 ++++++++++++++++++++++-- tests/trash.rs | 11 +++++------ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 8273a65..d47c8e1 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -301,7 +301,11 @@ where let original_path = item.original_path(); let trash_path = Path::new(&item.id); - std::fs::create_dir_all(&item.original_parent).map_err(|error| fs_error(&original_path, error))?; + // Ensure that both the trash item still exists, as well as that the + // there's no collision on the original path before proceeding. + if !std::fs::exists(&item.id).map_err(into_unknown)? { + return Err(Error::Unknown { description: format!("Trash item not found at {:?}", item.id) }); + } if std::fs::exists(&original_path).map_err(|error| fs_error(&original_path, error))? { return Err(Error::RestoreCollision { @@ -310,6 +314,7 @@ where }); } + std::fs::create_dir_all(&item.original_parent).map_err(|error| fs_error(&original_path, error))?; std::fs::rename(trash_path, &original_path).map_err(|error| fs_error(&original_path, error))?; } diff --git a/src/macos/tests.rs b/src/macos/tests.rs index be07395..ca8d6f2 100644 --- a/src/macos/tests.rs +++ b/src/macos/tests.rs @@ -2,10 +2,10 @@ use crate::{ macos::{percent_encode, DeleteMethod, TrashContextExtMacos}, restore_all, tests::{get_unique_name, init_logging}, - TrashContext, + Error, TrashContext, TrashItem, }; use serial_test::serial; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::fs::File; use std::os::unix::ffi::OsStrExt; use std::path::PathBuf; @@ -231,3 +231,23 @@ fn test_restore_all_restore_collision_finder() { _ => panic!("Calling delete_with_info with Finder method failed to return TrashItem."), }; } + +#[test] +fn test_restore_all_missing_trash_item() { + // Simulate providing a `TrashItem` to `restore_all` for a non-existing + // file, i.e., a file that isn't actually in the trash, so we can confirm + // that an error is returned. + // + // It doesn't matter that the `id` actually points to a file in the trash, + // we simply need to assert that `restore_all` checks whether `TrashItem.id` + // actually exists before attempting to restore it. + let id = std::env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); + let name: OsString = id.file_name().expect("Should be able to get the file name").into(); + let original_parent = id.parent().expect("Should be able to get parent").to_path_buf(); + let time_deleted = 0; + let trash_item = TrashItem { id: id.clone().into(), name, original_parent, time_deleted }; + + let err = restore_all(vec![trash_item]).expect_err("Should fail to restore non-existing file"); + let description = format!("Trash item not found at {:?}", id); + assert!(matches!(err, Error::Unknown { description: d } if d == description)) +} diff --git a/tests/trash.rs b/tests/trash.rs index 6db62da..d2abd0e 100644 --- a/tests/trash.rs +++ b/tests/trash.rs @@ -1,11 +1,10 @@ -use std::env; use std::fs::{create_dir, File}; use std::path::{Path, PathBuf}; use log::trace; use serial_test::serial; -use trash::{delete, delete_all, TrashContext}; +use trash::{delete, delete_all}; mod util { use std::sync::atomic::{AtomicI32, Ordering}; @@ -185,11 +184,11 @@ fn recursive_file_with_content_deletion() { fn test_delete_with_info() { // Create the test file to be deleted, ensuring that we include the current // directory so we can later assert that the `original_parent` is preserved. - let path = env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); + let path = std::env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); File::create_new(&path).unwrap(); // Create a new trash context for deleting the file with info. - let trash = TrashContext::new(); + let trash = trash::TrashContext::new(); match trash.delete_with_info(&path) { Ok(trash_item) => { @@ -221,11 +220,11 @@ fn test_delete_with_info() { // Create the test file to be deleted, ensuring that we include the current // directory so we can later assert that the `original_parent` is preserved. - let path = env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); + let path = std::env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); File::create_new(&path).unwrap(); // Create a new trash context for deleting the file with info. - let trash = TrashContext::new(); + let trash = trash::TrashContext::new(); match trash.delete_with_info(&path) { Ok(trash_item) => { From 8d07846e49ae98d288ab48b9e747283f4a77f0ee Mon Sep 17 00:00:00 2001 From: dino Date: Fri, 27 Feb 2026 15:38:56 +0000 Subject: [PATCH 6/6] test(macos): add test for twins and roundtrip --- src/macos/tests.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/src/macos/tests.rs b/src/macos/tests.rs index ca8d6f2..6f38836 100644 --- a/src/macos/tests.rs +++ b/src/macos/tests.rs @@ -176,6 +176,48 @@ fn test_delete_with_info_finder() { } } +#[test] +#[serial] +fn test_trash_and_restore_roundtrip_finder() { + let mut cleanup_paths = CleanupPaths::new(); + let path = std::env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); + cleanup_paths.push(path.clone()); + std::fs::write(&path, "Hello!").expect("Should be able to write to file"); + + let mut trash = TrashContext::new(); + trash.set_delete_method(DeleteMethod::Finder); + + let trash_item = trash.delete_with_info(&path).expect("Should be able to delete the file"); + assert!(!path.exists()); + + restore_all(vec![trash_item]).expect("Should successfully restore the trash item"); + + let file_contents = std::fs::read_to_string(&path).expect("Should be able to read file contents"); + assert!(path.exists()); + assert_eq!(file_contents, "Hello!"); +} + +#[test] +#[serial] +fn test_trash_and_restore_roundtrip_ns_file_manager() { + let mut cleanup_paths = CleanupPaths::new(); + let path = std::env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); + cleanup_paths.push(path.clone()); + std::fs::write(&path, "Hello!").expect("Should be able to write to file"); + + let mut trash = TrashContext::new(); + trash.set_delete_method(DeleteMethod::NsFileManager); + + let trash_item = trash.delete_with_info(&path).expect("Should be able to delete the file"); + assert!(!path.exists()); + + restore_all(vec![trash_item]).expect("Should successfully restore the trash item"); + + let file_contents = std::fs::read_to_string(&path).expect("Should be able to read file contents"); + assert!(path.exists()); + assert_eq!(file_contents, "Hello!"); +} + #[test] #[serial] fn test_restore_all_restore_collision_file_manager() { @@ -247,7 +289,29 @@ fn test_restore_all_missing_trash_item() { let time_deleted = 0; let trash_item = TrashItem { id: id.clone().into(), name, original_parent, time_deleted }; - let err = restore_all(vec![trash_item]).expect_err("Should fail to restore non-existing file"); - let description = format!("Trash item not found at {:?}", id); - assert!(matches!(err, Error::Unknown { description: d } if d == description)) + match restore_all(vec![trash_item]) { + Err(Error::Unknown { description }) => assert_eq!(description, format!("Trash item not found at {:?}", id)), + _ => panic!("Should fail to restore non-existing file"), + } +} + +#[test] +fn test_restore_all_twins() { + let id = std::env::current_dir().expect("Should be able to get current directory").join(get_unique_name()); + let name: OsString = id.file_name().expect("Should be able to get the file name").into(); + let original_parent = id.parent().expect("Should be able to get parent").to_path_buf(); + let time_deleted = 0; + + let trash_items = vec![ + TrashItem { id: id.clone().into(), name: name.clone(), original_parent: original_parent.clone(), time_deleted }, + TrashItem { id: id.clone().into(), name, original_parent, time_deleted }, + ]; + + match restore_all(trash_items.clone()) { + Err(Error::RestoreTwins { path, items }) => { + assert_eq!(path, id); + assert_eq!(items, trash_items); + } + _ => panic!("Should return Error::RestoreTwins"), + } }