From cbf4c26dda83bf3aeae561c832aa4747a4ef6a97 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 11 Jul 2024 16:04:55 -0600 Subject: [PATCH 1/2] Return trash items that are created by delete --- src/freedesktop.rs | 29 ++++++++++++++++++++--------- src/lib.rs | 8 ++++---- src/macos/mod.rs | 9 +++++---- src/windows.rs | 12 +++++++----- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/freedesktop.rs b/src/freedesktop.rs index a5de1af..78b23db 100644 --- a/src/freedesktop.rs +++ b/src/freedesktop.rs @@ -33,12 +33,13 @@ impl PlatformTrashContext { } } impl TrashContext { - pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec) -> Result<(), Error> { + pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec) -> Result>, Error> { let home_trash = home_trash()?; let sorted_mount_points = get_sorted_mount_points()?; let home_topdir = home_topdir(&sorted_mount_points)?; debug!("The home topdir is {:?}", home_topdir); let uid = unsafe { libc::getuid() }; + let mut items = Vec::with_capacity(full_paths.len()); for path in full_paths { debug!("Deleting {:?}", path); let topdir = get_first_topdir_containing_path(&path, &sorted_mount_points); @@ -47,18 +48,19 @@ impl TrashContext { debug!("The topdir was identical to the home topdir, so moving to the home trash."); // Note that the following function creates the trash folder // and its required subfolders in case they don't exist. - move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?; + items.push(move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?); } else if topdir.to_str() == Some("/var/home") && home_topdir.to_str() == Some("/") { debug!("The topdir is '/var/home' but the home_topdir is '/', moving to the home trash anyway."); - move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?; + items.push(move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?); } else { execute_on_mounted_trash_folders(uid, topdir, true, true, |trash_path| { - move_to_trash(&path, trash_path, topdir) + items.push(move_to_trash(&path, trash_path, topdir)?); + Ok(()) }) .map_err(|(p, e)| fs_error(p, e))?; } } - Ok(()) + Ok(Some(items)) } } @@ -450,7 +452,7 @@ fn move_to_trash( src: impl AsRef, trash_folder: impl AsRef, _topdir: impl AsRef, -) -> Result<(), FsError> { +) -> Result { let src = src.as_ref(); let trash_folder = trash_folder.as_ref(); let files_folder = trash_folder.join("files"); @@ -491,6 +493,7 @@ fn move_to_trash( info_name.push(".trashinfo"); let info_file_path = info_folder.join(&info_name); let info_result = OpenOptions::new().create_new(true).write(true).open(&info_file_path); + let mut time_deleted = -1; match info_result { Err(error) => { if error.kind() == std::io::ErrorKind::AlreadyExists { @@ -510,10 +513,12 @@ fn move_to_trash( #[cfg(feature = "chrono")] { let now = chrono::Local::now(); + time_deleted = now.timestamp(); writeln!(file, "DeletionDate={}", now.format("%Y-%m-%dT%H:%M:%S")) } #[cfg(not(feature = "chrono"))] { + time_deleted = -1; Ok(()) } }) @@ -537,12 +542,18 @@ fn move_to_trash( } Ok(_) => { // We did it! - break; + return Ok(TrashItem { + id: info_file_path.into(), + name: filename.into(), + original_parent: src + .parent() + .expect("Absolute path to trashed item should have a parent") + .to_path_buf(), + time_deleted, + }); } } } - - Ok(()) } /// An error may mean that a collision was found. diff --git a/src/lib.rs b/src/lib.rs index adccaea..42cf31d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,7 +81,7 @@ impl TrashContext { /// trash::delete("delete_me").unwrap(); /// assert!(File::open("delete_me").is_err()); /// ``` - pub fn delete>(&self, path: T) -> Result<(), Error> { + pub fn delete>(&self, path: T) -> Result>, Error> { self.delete_all(&[path]) } @@ -101,7 +101,7 @@ impl TrashContext { /// assert!(File::open("delete_me_1").is_err()); /// assert!(File::open("delete_me_2").is_err()); /// ``` - pub fn delete_all(&self, paths: I) -> Result<(), Error> + pub fn delete_all(&self, paths: I) -> Result>, Error> where I: IntoIterator, T: AsRef, @@ -116,14 +116,14 @@ impl TrashContext { /// Convenience method for `DEFAULT_TRASH_CTX.delete()`. /// /// See: [`TrashContext::delete`](TrashContext::delete) -pub fn delete>(path: T) -> Result<(), Error> { +pub fn delete>(path: T) -> Result>, Error> { DEFAULT_TRASH_CTX.delete(path) } /// Convenience method for `DEFAULT_TRASH_CTX.delete_all()`. /// /// See: [`TrashContext::delete_all`](TrashContext::delete_all) -pub fn delete_all(paths: I) -> Result<(), Error> +pub fn delete_all(paths: I) -> Result>, Error> where I: IntoIterator, T: AsRef, diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 62027bd..6e3db40 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -7,7 +7,7 @@ use std::{ use log::trace; use objc2_foundation::{NSFileManager, NSString, NSURL}; -use crate::{into_unknown, Error, TrashContext}; +use crate::{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 @@ -74,11 +74,12 @@ impl TrashContextExtMacos for TrashContext { } } impl TrashContext { - pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec) -> Result<(), Error> { + pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec) -> Result>, Error> { match self.platform_specific.delete_method { - DeleteMethod::Finder => delete_using_finder(&full_paths), - DeleteMethod::NsFileManager => delete_using_file_mgr(&full_paths), + DeleteMethod::Finder => delete_using_finder(&full_paths)?, + DeleteMethod::NsFileManager => delete_using_file_mgr(&full_paths)?, } + Ok(None) } } diff --git a/src/windows.rs b/src/windows.rs index 8727706..7b97bef 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -36,7 +36,10 @@ impl PlatformTrashContext { } impl TrashContext { /// See https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-_shfileopstructa - pub(crate) fn delete_specified_canonicalized(&self, full_paths: Vec) -> Result<(), Error> { + pub(crate) fn delete_specified_canonicalized( + &self, + full_paths: Vec, + ) -> Result>, Error> { ensure_com_initialized(); unsafe { let pfo: IFileOperation = CoCreateInstance(&FileOperation as *const _, None, CLSCTX_ALL).unwrap(); @@ -66,14 +69,13 @@ impl TrashContext { // the list of HRESULT codes is not documented. return Err(Error::Unknown { description: "Some operations were aborted".into() }); } - Ok(()) + Ok(None) } } /// Removes all files and folder paths recursively. - pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec) -> Result<(), Error> { - self.delete_specified_canonicalized(full_paths)?; - Ok(()) + pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec) -> Result>, Error> { + self.delete_specified_canonicalized(full_paths) } } From b21bf811ed7fdb51b675fc7805d704134c29cac2 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Tue, 24 Dec 2024 02:33:42 -0500 Subject: [PATCH 2/2] Add unit test for `delete-info` The unit test deletes a few files including one with a name that is invalid UTF-8. It then tests if the deleted names are correctly returned. The test is disabled for the usual targets, such as macOS. --- src/tests.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/tests.rs b/src/tests.rs index 41ad08a..34ee4b5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -297,4 +297,36 @@ mod os_limited { let is_empty = trash::os_limited::is_empty().unwrap(); assert_eq!(is_empty, is_empty_list, "is_empty() should match empty status from list()"); } + + #[cfg(all(unix, not(target_os = "macos"), not(target_os = "ios"), not(target_os = "android")))] + #[test] + #[serial] + fn delete_items_info() { + init_logging(); + let names = { + let prefix = get_unique_name(); + let mut names = Vec::with_capacity(5); + for n in 0..4 { + let name = format!("{prefix}#{n}").into_bytes(); + names.push(OsString::from_vec(name)); + } + + // Throw in an invalid UTF-8 OsString for good measure + let mut name = OsStr::new(&format!("{prefix}#")).to_os_string().into_encoded_bytes(); + name.push(168); + names.push(OsString::from_vec(name)); + + names + }; + + for name in &names { + File::create_new(name).unwrap(); + } + + let deleted_names = trash::delete_all(&names).unwrap().expect("Should get a list of deleted items"); + assert_eq!(deleted_names.len(), names.len()); + for name in names { + assert!(deleted_names.iter().any(|item| item.name == name)); + } + } }