diff --git a/Cargo.toml b/Cargo.toml index 91ef201..a8810e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,14 @@ coinit_speed_over_memory = [] [dev-dependencies] chrono = "0.4.9" rand = "0.7.2" -lazy_static = "1.4.0" +once_cell = "1.7.2" +env_logger = "0.8" [dependencies] +log = "0.4" + +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2.7" [target.'cfg(target_os = "linux")'.dependencies] chrono = "0.4.9" @@ -37,11 +42,3 @@ windows = "0.8.0" [target.'cfg(windows)'.build-dependencies] windows = "0.8.0" - -# [target.'cfg(windows)'.dependencies.winapi] -# git = "https://github.com/ArturKovacs/winapi-rs.git" -# rev = "fe297b9830c804b274e2206d3aebfe9dc33e39cb" -# features = [ -# "combaseapi", "objbase", "objidl", "shlobj", "shellapi", "winerror", "shlobj", "shlwapi", "shobjidl_core", -# "shobjidl", "oleauto", "oaidl", "wtypes", "errhandlingapi", "timezoneapi", "winuser" -# ] diff --git a/examples/delete_method.rs b/examples/delete_method.rs new file mode 100644 index 0000000..1c699cf --- /dev/null +++ b/examples/delete_method.rs @@ -0,0 +1,23 @@ +use std::fs::File; +use trash::TrashContext; + +#[cfg(target_os = "macos")] +use trash::macos::{DeleteMethod, TrashContextExtMacos}; + +#[cfg(not(target_os = "macos"))] +fn main() { + println!("This example is only available on macOS"); +} + +#[cfg(target_os = "macos")] +fn main() { + env_logger::init(); + + let mut trash_ctx = TrashContext::default(); + trash_ctx.set_delete_method(DeleteMethod::NSFileManager); + + let path = "this_file_was_deleted_using_the_ns_file_manager"; + File::create(path).unwrap(); + trash_ctx.delete(path).unwrap(); + assert!(File::open(path).is_err()); +} diff --git a/src/lib.rs b/src/lib.rs index e4dd988..18b1630 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,8 +38,87 @@ mod platform; mod platform; #[cfg(target_os = "macos")] -#[path = "macos.rs"] -mod platform; +pub mod macos; +use log::trace; +#[cfg(target_os = "macos")] +use macos as platform; + +// pub use platform as my_latform; +pub const DEFAULT_TRASH_CTX: TrashContext = TrashContext::new(); + +/// A collection of preferences for trash operations. +#[derive(Clone, Default, Debug)] +pub struct TrashContext { + platform_specific: platform::PlatformTrashContext, +} +impl TrashContext { + pub const fn new() -> Self { + Self { platform_specific: platform::PlatformTrashContext::new() } + } + + /// Removes a single file or directory. + /// + /// When a symbolic link is provided to this function, the sybolic link will be removed and the link + /// target will be kept intact. + /// + /// # Example + /// + /// ``` + /// use std::fs::File; + /// use trash::delete; + /// File::create("delete_me").unwrap(); + /// trash::delete("delete_me").unwrap(); + /// assert!(File::open("delete_me").is_err()); + /// ``` + pub fn delete>(&self, path: T) -> Result<(), Error> { + self.delete_all(&[path]) + } + + /// Removes all files/directories specified by the collection of paths provided as an argument. + /// + /// When a symbolic link is provided to this function, the sybolic link will be removed and the link + /// target will be kept intact. + /// + /// # Example + /// + /// ``` + /// use std::fs::File; + /// use trash::delete_all; + /// File::create("delete_me_1").unwrap(); + /// File::create("delete_me_2").unwrap(); + /// delete_all(&["delete_me_1", "delete_me_2"]).unwrap(); + /// 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> + where + I: IntoIterator, + T: AsRef, + { + trace!("Starting canonicalize_paths"); + let full_paths = canonicalize_paths(paths)?; + trace!("Finished canonicalize_paths"); + self.delete_all_canonicalized(full_paths) + } +} + +/// Convenience method for `DEFAULT_TRASH_CTX.delete()`. +/// +/// See: [`TrashContext::delete`](TrashContext::delete) +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> +where + I: IntoIterator, + T: AsRef, +{ + DEFAULT_TRASH_CTX.delete_all(paths) +} /// /// A type that is contained within [`Error`]. It provides information about why the error was @@ -164,49 +243,6 @@ where .collect::, _>>() } -/// Removes a single file or directory. -/// -/// When a symbolic link is provided to this function, the sybolic link will be removed and the link -/// target will be kept intact. -/// -/// # Example -/// -/// ``` -/// use std::fs::File; -/// use trash::delete; -/// File::create("delete_me").unwrap(); -/// trash::delete("delete_me").unwrap(); -/// assert!(File::open("delete_me").is_err()); -/// ``` -pub fn delete>(path: T) -> Result<(), Error> { - delete_all(&[path]) -} - -/// Removes all files/directories specified by the collection of paths provided as an argument. -/// -/// When a symbolic link is provided to this function, the sybolic link will be removed and the link -/// target will be kept intact. -/// -/// # Example -/// -/// ``` -/// use std::fs::File; -/// use trash::delete_all; -/// File::create("delete_me_1").unwrap(); -/// File::create("delete_me_2").unwrap(); -/// delete_all(&["delete_me_1", "delete_me_2"]).unwrap(); -/// assert!(File::open("delete_me_1").is_err()); -/// assert!(File::open("delete_me_2").is_err()); -/// ``` -pub fn delete_all(paths: I) -> Result<(), Error> -where - I: IntoIterator, - T: AsRef, -{ - let full_paths = canonicalize_paths(paths)?; - platform::delete_all_canonicalized(full_paths) -} - /// This struct holds information about a single item within the trash. /// /// Some functions associated with this struct are defined in the `TrahsItemPlatformDep` trait. @@ -263,6 +299,9 @@ impl Hash for TrashItem { #[cfg(any(target_os = "windows", all(unix, not(target_os = "macos"))))] pub mod extra { + // TODO: Rename this to os_limited + // TODO: rename the linux module to `freedesktop` + use std::{ collections::HashSet, hash::{Hash, Hasher}, @@ -270,6 +309,17 @@ pub mod extra { use super::{platform, Error, TrashItem}; + pub trait TrashContextExtOsLimited { + fn list() -> Result, Error>; + fn purge_all(items: I) -> Result<(), Error> + where + I: IntoIterator; + + fn restore_all(items: I) -> Result<(), Error> + where + I: IntoIterator; + } + /// Returns all [`TrashItem`]s that are currently in the trash. /// /// The items are in no particular order and must be sorted when any kind of ordering is required. diff --git a/src/linux.rs b/src/linux.rs index 736dba6..1ed15ab 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -19,6 +19,9 @@ use scopeguard::defer; use crate::{Error, TrashItem}; +#[derive(Clone, Default, Debug)] +pub struct PlatformTrashContext; + pub fn delete_all_canonicalized(full_paths: Vec) -> Result<(), Error> { let root = Path::new("/"); let home_trash = home_trash()?; diff --git a/src/macos.rs b/src/macos.rs index 456dc67..d35393d 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -1,11 +1,138 @@ -use std::ffi::OsString; -use std::path::PathBuf; -use std::process::Command; +use std::{ffi::OsString, path::PathBuf, process::Command}; -use crate::{into_unknown, Error}; +use log::{trace, warn}; +use objc::{ + class, msg_send, + runtime::{Object, BOOL, NO}, + sel, sel_impl, +}; -pub fn delete_all_canonicalized(full_paths: Vec) -> Result<(), Error> { - let full_paths = full_paths.into_iter().map(to_string).collect::, _>>()?; +use crate::{into_unknown, Error, TrashContext}; + +#[link(name = "Foundation", kind = "framework")] +extern "C" { + // Using an empty scope to just link against the foundation framework, + // to find the NSFileManager, but we don't need anything else from it. +} + +#[allow(non_camel_case_types)] +type id = *mut Object; +#[allow(non_upper_case_globals)] +const nil: id = std::ptr::null_mut(); +#[allow(non_upper_case_globals)] +const NSUTF8StringEncoding: usize = 4; + +#[derive(Copy, Clone, Debug)] +pub enum DeleteMethod { + /// Use `trashItemAtURL` from the `NSFileManager` object to delete the files. + /// + /// This seems to be somewhat faster and it does not produce the sound effect for when moving + /// items to the Trash with Finder. + /// + /// This is the default. + NSFileManager, + + /// Use an `osascript` asking the Finder application to delete the files. + /// + /// This will produce the sound effect that Finder usualy makes when moving a file to the + /// Trash. + Finder, +} +impl DeleteMethod { + pub const fn new() -> Self { + DeleteMethod::NSFileManager + } +} +impl Default for DeleteMethod { + fn default() -> Self { + Self::new() + } +} +#[derive(Clone, Default, Debug)] +pub struct PlatformTrashContext { + delete_method: DeleteMethod, +} +impl PlatformTrashContext { + pub const fn new() -> Self { + Self { delete_method: DeleteMethod::new() } + } +} +pub trait TrashContextExtMacos { + fn set_delete_method(&mut self, method: DeleteMethod); + fn delete_method(&self) -> DeleteMethod; +} +impl TrashContextExtMacos for TrashContext { + fn set_delete_method(&mut self, method: DeleteMethod) { + self.platform_specific.delete_method = method; + } + fn delete_method(&self) -> DeleteMethod { + self.platform_specific.delete_method + } +} +impl TrashContext { + pub fn delete_all_canonicalized(&self, full_paths: Vec) -> Result<(), Error> { + let full_paths = full_paths.into_iter().map(to_string).collect::, _>>()?; + match self.platform_specific.delete_method { + DeleteMethod::Finder => delete_using_finder(full_paths), + DeleteMethod::NSFileManager => delete_using_file_mgr(full_paths), + } + } +} + +fn delete_using_file_mgr(full_paths: Vec) -> Result<(), Error> { + trace!("Starting delete_using_file_mgr"); + let url_cls = class!(NSURL); + let file_mgr_cls = class!(NSFileManager); + let file_mgr: id = unsafe { msg_send![file_mgr_cls, defaultManager] }; + for path in full_paths { + let string = to_ns_string(&path); + trace!("Starting fileURLWithPath"); + let url: id = unsafe { msg_send![url_cls, fileURLWithPath:string.ptr] }; + if url == nil { + return Err(Error::Unknown { + description: format!("Failed to convert a path to an NSURL. Path: '{}'", path), + }); + } + trace!("Finished fileURLWithPath"); + // WARNING: I don't know why but if we try to call release on the url, it sometimes + // crashes with SIGSEGV, so we instead don't try to release the url + // let url = OwnedObject { ptr: url }; + let mut error: id = nil; + trace!("Calling trashItemAtURL"); + let success: BOOL = unsafe { + msg_send![ + file_mgr, + trashItemAtURL:url + resultingItemURL:nil + error:(&mut error as *mut id) + ] + }; + trace!("Finished trashItemAtURL"); + if success == NO { + trace!("success was NO"); + if error == nil { + return Err(Error::Unknown { + description: format!( + "While deleting '{}', `trashItemAtURL` returned with failure but no error was specified.", + path + ) + }); + } + let code: isize = unsafe { msg_send![error, code] }; + let domain: id = unsafe { msg_send![error, domain] }; + let domain = unsafe { ns_string_to_rust(domain)? }; + return Err(Error::Unknown { + description: format!( + "While deleting '{}', `trashItemAtURL` failed, code: {}, domain: {}", + path, code, domain + ), + }); + } + } + Ok(()) +} + +fn delete_using_finder(full_paths: Vec) -> Result<(), Error> { // AppleScript command to move files (or directories) to Trash looks like // osascript -e 'tell application "Finder" to delete { POSIX file "file1", POSIX "file2" }' // The `-e` flag is used to execute only one line of AppleScript. @@ -35,33 +162,6 @@ pub fn delete_all_canonicalized(full_paths: Vec) -> Result<(), Error> { Ok(()) } -// pub fn remove_all(paths: I) -> Result<(), Error> -// where -// I: IntoIterator, -// T: AsRef, -// { -// let full_paths = paths -// .into_iter() -// // Convert paths into canonical, absolute forms and collect errors -// .map(|path| { -// path.as_ref().canonicalize().map_err(|e| { -// Error::new( -// ErrorKind::CanonicalizePath { -// original: path.as_ref().into(), -// }, -// Box::new(e), -// ) -// }) -// }) -// .collect::, Error>>()?; - -// remove_all_canonicalized(full_paths) -// } - -// pub fn remove>(path: T) -> Result<(), Error> { -// remove_all(&[path]) -// } - fn to_string>(str_in: T) -> Result { let os_string = str_in.into(); let s = os_string.to_str(); @@ -73,3 +173,46 @@ fn to_string>(str_in: T) -> Result { } } } + +/// Uses the Drop trait to `release` the object held by `ptr`. +#[repr(transparent)] +struct OwnedObject { + pub ptr: id, +} +impl Drop for OwnedObject { + fn drop(&mut self) { + let () = unsafe { msg_send![self.ptr, release] }; + } +} + +fn to_ns_string(s: &str) -> OwnedObject { + trace!("Called to_ns_string on '{}'", s); + let utf8 = s.as_bytes(); + let string_cls = class!(NSString); + let alloced_string: id = unsafe { msg_send![string_cls, alloc] }; + let mut string: id = unsafe { + msg_send![ + alloced_string, + initWithBytes:utf8.as_ptr() + length:utf8.len() + encoding:NSUTF8StringEncoding + ] + }; + if string == nil { + warn!("initWithBytes returned nil when trying to convert a rust string to an NSString"); + string = unsafe { msg_send![alloced_string, init] }; + } + OwnedObject { ptr: string } +} + +/// Safety: `string` is assumed to be a pointer to an NSString +unsafe fn ns_string_to_rust(string: id) -> Result { + if string == nil { + return Ok(String::new()); + } + let utf8_bytes: *const u8 = msg_send![string, UTF8String]; + let utf8_len: usize = msg_send![string, lengthOfBytesUsingEncoding: NSUTF8StringEncoding]; + let str_slice = std::slice::from_raw_parts(utf8_bytes, utf8_len); + let rust_str = std::str::from_utf8(str_slice).map_err(into_unknown)?; + Ok(rust_str.to_owned()) +} diff --git a/src/tests.rs b/src/tests.rs index 1ca0464..6fb3679 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4,44 +4,57 @@ use std::path::PathBuf; use std::sync::atomic::{AtomicI64, Ordering}; use chrono; -use lazy_static::lazy_static; +use log::trace; +use once_cell::sync::Lazy; -#[allow(deprecated)] use crate::{delete, delete_all}; // WARNING Expecting that `cargo test` won't be invoked on the same computer more than once within // a single millisecond -lazy_static! { - static ref INSTANCE_ID: i64 = chrono::Local::now().timestamp_millis(); - static ref ID_OFFSET: AtomicI64 = AtomicI64::new(0); -} +static INSTANCE_ID: Lazy = Lazy::new(|| chrono::Local::now().timestamp_millis()); +static ID_OFFSET: AtomicI64 = AtomicI64::new(0); pub fn get_unique_name() -> String { let id = ID_OFFSET.fetch_add(1, Ordering::SeqCst); format!("trash-test-{}-{}", *INSTANCE_ID, id) } +fn init_logging() { + let _ = env_logger::builder().is_test(true).try_init(); +} + #[test] fn test_delete_file() { - let path = "test_file_to_delete"; - File::create(path).unwrap(); + init_logging(); + trace!("Started test_delete_file"); - delete(path).unwrap(); - assert!(File::open(path).is_err()); + let path = get_unique_name(); + File::create(&path).unwrap(); + + delete(&path).unwrap(); + assert!(File::open(&path).is_err()); + trace!("Finished test_delete_file"); } #[test] fn test_delete_folder() { - let path = PathBuf::from("test_folder_to_delete"); + init_logging(); + trace!("Started test_delete_folder"); + + let path = PathBuf::from(get_unique_name()); create_dir(&path).unwrap(); File::create(path.join("file_in_folder")).unwrap(); assert!(path.exists()); delete(&path).unwrap(); assert!(path.exists() == false); + + trace!("Finished test_delete_folder"); } #[test] fn test_delete_all() { + init_logging(); + trace!("Started test_delete_all"); let count: usize = 3; let paths: Vec<_> = (0..count).map(|i| format!("test_file_to_delete_{}", i)).collect(); @@ -53,34 +66,44 @@ fn test_delete_all() { for path in paths.iter() { assert!(File::open(path).is_err()); } + trace!("Finished test_delete_all"); } #[cfg(unix)] mod unix { - #[allow(deprecated)] + use log::trace; + use std::{ + fs::{create_dir, remove_dir_all, remove_file, File}, + os::unix::fs::symlink, + path::Path, + }; + + use super::{get_unique_name, init_logging}; use crate::delete; - use std::fs::{create_dir, remove_dir_all, remove_file, File}; - use std::os::unix::fs::symlink; - - use std::path::Path; + // use crate::init_logging; #[test] fn test_delete_symlink() { - let target_path = "test_link_target_for_delete"; - File::create(target_path).unwrap(); + init_logging(); + trace!("Started test_delete_symlink"); + let target_path = get_unique_name(); + File::create(&target_path).unwrap(); let link_path = "test_link_to_delete"; - symlink(target_path, link_path).unwrap(); + symlink(&target_path, link_path).unwrap(); delete(link_path).unwrap(); assert!(File::open(link_path).is_err()); - assert!(File::open(target_path).is_ok()); + assert!(File::open(&target_path).is_ok()); // Cleanup - remove_file(target_path).unwrap(); + remove_file(&target_path).unwrap(); + trace!("Finished test_delete_symlink"); } #[test] fn test_delete_symlink_in_folder() { + init_logging(); + trace!("Started test_delete_symlink_in_folder"); let target_path = "test_link_target_for_delete_from_folder"; File::create(target_path).unwrap(); @@ -95,6 +118,7 @@ mod unix { // Cleanup remove_file(target_path).unwrap(); remove_dir_all(folder).unwrap(); + trace!("Finished test_delete_symlink_in_folder"); } } @@ -106,6 +130,7 @@ mod extra { #[test] fn list() { + init_logging(); let file_name_prefix = get_unique_name(); let batches: usize = 2; let files_per_batch: usize = 3; @@ -146,16 +171,19 @@ mod extra { #[test] fn purge_empty() { + init_logging(); trash::extra::purge_all(vec![]).unwrap(); } #[test] fn restore_empty() { + init_logging(); trash::extra::restore_all(vec![]).unwrap(); } #[test] fn purge() { + init_logging(); let file_name_prefix = get_unique_name(); let batches: usize = 2; let files_per_batch: usize = 3; @@ -186,6 +214,7 @@ mod extra { #[test] fn restore() { + init_logging(); let file_name_prefix = get_unique_name(); let file_count: usize = 3; let names: Vec<_> = @@ -223,6 +252,7 @@ mod extra { #[test] fn restore_collision() { + init_logging(); let file_name_prefix = get_unique_name(); let file_count: usize = 3; let collision_remaining = file_count - 1; @@ -278,6 +308,7 @@ mod extra { #[test] fn restore_twins() { + init_logging(); let file_name_prefix = get_unique_name(); let file_count: usize = 4; let names: Vec<_> = diff --git a/src/windows.rs b/src/windows.rs index 2fcd4c0..9b6aed7 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -44,6 +44,9 @@ const FOF_NO_UI: u32 = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_NOC const FOFX_EARLYFAILURE: u32 = 0x00100000; /////////////////////////////////////////////////////////////////////////// +#[derive(Clone, Default, Debug)] +pub struct PlatformTrashContext; + macro_rules! check_hresult { {$f_name:ident($($args:tt)*)} => ({ let hr = $f_name($($args)*);