Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ objc2-foundation = { version = "0.3.2", default-features = false, features = [
"std",
"NSError",
"NSFileManager",
"NSPathUtilities",
"NSString",
"NSURL",
] }
Expand All @@ -55,12 +56,14 @@ once_cell = "1.7.2"

[target.'cfg(windows)'.dependencies]
windows = { version = "0.56.0", features = [
"implement",
"Win32_Foundation",
"Win32_Storage_EnhancedStorage",
"Win32_System_Com_StructuredStorage",
"Win32_System_SystemServices",
"Win32_UI_Shell_PropertiesSystem",
] }
windows-core = "0.56.0"
scopeguard = "1.2.0"

# workaround for https://github.com/cross-rs/cross/issues/1345
Expand Down
47 changes: 38 additions & 9 deletions src/freedesktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::{
fs::PermissionsExt,
},
path::{Component, Path, PathBuf},
time::SystemTime,
};

use log::{debug, warn};
Expand All @@ -33,10 +34,16 @@ impl PlatformTrashContext {
}
}
impl TrashContext {
pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {
pub(crate) fn delete_all_canonicalized(
&self,
full_paths: Vec<PathBuf>,
with_info: bool,
) -> Result<Option<Vec<TrashItem>>, Error> {
let home_trash = home_trash()?;
let sorted_mount_points = get_sorted_mount_points()?;
let home_topdir = home_topdir(&sorted_mount_points)?;
let mut items: Vec<TrashItem> = Vec::new();

debug!("The home topdir is {:?}", home_topdir);
let uid = unsafe { libc::getuid() };
for path in full_paths {
Expand All @@ -47,18 +54,37 @@ 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))?;
let item = move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?;
if with_info {
items.push(item);
}
} 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))?;
let item = move_to_trash(path, &home_trash, topdir).map_err(|(p, e)| fs_error(p, e))?;
if with_info {
items.push(item);
}
} else {
let mut item = None;
execute_on_mounted_trash_folders(uid, topdir, true, true, |trash_path| {
move_to_trash(&path, trash_path, topdir)
let trash_item = move_to_trash(&path, trash_path, topdir)?;
item = Some(trash_item);
Ok(())
})
.map_err(|(p, e)| fs_error(p, e))?;
if with_info {
if let Some(trash_item) = item {
items.push(trash_item);
}
}
}
}
Ok(())

if with_info {
Ok(Some(items))
} else {
Ok(None)
}
}
}

Expand Down Expand Up @@ -450,7 +476,7 @@ fn move_to_trash(
src: impl AsRef<Path>,
trash_folder: impl AsRef<Path>,
_topdir: impl AsRef<Path>,
) -> Result<(), FsError> {
) -> Result<TrashItem, FsError> {
let src = src.as_ref();
let trash_folder = trash_folder.as_ref();
let files_folder = trash_folder.join("files");
Expand Down Expand Up @@ -537,12 +563,15 @@ fn move_to_trash(
}
Ok(_) => {
// We did it!
break;
let original_parent = src.parent().map(Path::to_owned).unwrap_or_default();
let name = src.file_name().map(OsStr::to_owned).unwrap_or_default();
let time_deleted =
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(-1);

return Ok(TrashItem { id: info_file_path.into_os_string(), name, original_parent, time_deleted });
}
}
}

Ok(())
}

/// An error may mean that a collision was found.
Expand Down
45 changes: 44 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,32 @@ impl TrashContext {
trace!("Starting canonicalize_paths");
let full_paths = canonicalize_paths(paths)?;
trace!("Finished canonicalize_paths");
self.delete_all_canonicalized(full_paths)

self.delete_all_canonicalized(full_paths, false).map(|_| ())
}

/// Same as `delete`, but returns a `TrashItem` describing where the file
/// ended up in trash.
pub fn delete_with_info<T: AsRef<Path>>(&self, path: T) -> Result<TrashItem, Error> {
self.delete_all_with_info(&[path])?
.pop()
.ok_or(Error::Unknown { description: "delete_with_info did not return trash item information".into() })
}

/// Same as `delete_all` but returns `TrashItem`s describing where files
/// ended up in the trash.
pub fn delete_all_with_info<I, T>(&self, paths: I) -> Result<Vec<TrashItem>, Error>
where
I: IntoIterator<Item = T>,
T: AsRef<Path>,
{
let full_paths = canonicalize_paths(paths)?;

self.delete_all_canonicalized(full_paths, true).and_then(|items| {
items.ok_or(Error::Unknown {
description: "delete_all_with_info did not return trash item information".into(),
})
})
}
}

Expand All @@ -120,6 +145,13 @@ pub fn delete<T: AsRef<Path>>(path: T) -> Result<(), Error> {
DEFAULT_TRASH_CTX.delete(path)
}

/// Convenience method for `DEFAULT_TRASH_CTX.delete_with_info()`.
///
/// See: [`TrashContext::delete_with_info`](TrashContext::delete_with_info)
pub fn delete_with_info<T: AsRef<Path>>(path: T) -> Result<TrashItem, Error> {
DEFAULT_TRASH_CTX.delete_with_info(path)
}

/// Convenience method for `DEFAULT_TRASH_CTX.delete_all()`.
///
/// See: [`TrashContext::delete_all`](TrashContext::delete_all)
Expand All @@ -131,6 +163,17 @@ where
DEFAULT_TRASH_CTX.delete_all(paths)
}

/// Convenience method for `DEFAULT_TRASH_CTX.delete_all_with_info()`.
///
/// See: [`TrashContext::delete_all_with_info`](TrashContext::delete_all_with_info)
pub fn delete_all_with_info<I, T>(paths: I) -> Result<Vec<TrashItem>, Error>
where
I: IntoIterator<Item = T>,
T: AsRef<Path>,
{
DEFAULT_TRASH_CTX.delete_all_with_info(paths)
}

/// Provides information about an error.
#[derive(Debug)]
pub enum Error {
Expand Down
123 changes: 104 additions & 19 deletions src/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use std::{
ffi::OsString,
path::{Path, PathBuf},
process::Command,
time::SystemTime,
};

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
Expand All @@ -26,7 +27,6 @@ pub enum DeleteMethod {
/// - Produces the sound that Finder usually makes when deleting a file
/// - Shows the "Put Back" option in the context menu, when using the Finder application
///
/// This is the default.
Finder,

/// Use `trashItemAtURL` from the `NSFileManager` object to delete the files.
Expand All @@ -39,12 +39,14 @@ pub enum DeleteMethod {
/// at:
/// - <https://github.com/sindresorhus/macos-trash/issues/4>
/// - <https://github.com/ArturKovacs/trash-rs/issues/14>
///
/// This is the default.
NsFileManager,
}
impl DeleteMethod {
/// Returns `DeleteMethod::Finder`
/// Returns `DeleteMethod::NsFileManager`
pub const fn new() -> Self {
DeleteMethod::Finder
DeleteMethod::NsFileManager
}
}
impl Default for DeleteMethod {
Expand Down Expand Up @@ -74,17 +76,23 @@ impl TrashContextExtMacos for TrashContext {
}
}
impl TrashContext {
pub(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {
pub(crate) fn delete_all_canonicalized(
&self,
full_paths: Vec<PathBuf>,
with_info: bool,
) -> Result<Option<Vec<TrashItem>>, 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, with_info),
DeleteMethod::NsFileManager => delete_using_file_mgr(&full_paths, with_info),
}
}
}

fn delete_using_file_mgr<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error> {
fn delete_using_file_mgr<P: AsRef<Path>>(full_paths: &[P], with_info: bool) -> Result<Option<Vec<TrashItem>>, Error> {
trace!("Starting delete_using_file_mgr");
let file_mgr = NSFileManager::defaultManager();
let mut trash_items = Vec::<TrashItem>::new();

for path in full_paths {
let path = path.as_ref().as_os_str().as_encoded_bytes();
let path = match std::str::from_utf8(path) {
Expand All @@ -96,36 +104,83 @@ fn delete_using_file_mgr<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error>
let url = NSURL::fileURLWithPath(&path);
trace!("Finished fileURLWithPath");

let mut trash_url = None;

trace!("Calling trashItemAtURL");
let res = file_mgr.trashItemAtURL_resultingItemURL_error(&url, None);
let res = file_mgr.trashItemAtURL_resultingItemURL_error(&url, Some(&mut trash_url));
trace!("Finished trashItemAtURL");

if let Err(err) = res {
return Err(Error::Unknown {
description: format!("While deleting '{:?}', `trashItemAtURL` failed: {err}", &path),
});
}

if with_info {
trash_items.push(TrashItem {
name: OsString::from(path.lastPathComponent().to_string()),
original_parent: path.stringByDeletingLastPathComponent().to_string().into(),
id: trash_url
.and_then(|url| url.path())
.map(|path| OsString::from(path.to_string()))
.unwrap_or_default(),
time_deleted: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or(-1),
});
}
}

if with_info {
Ok(Some(trash_items))
} else {
Ok(None)
}
Ok(())
}

fn delete_using_finder<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error> {
fn delete_using_finder<P: AsRef<Path>>(full_paths: &[P], with_info: bool) -> Result<Option<Vec<TrashItem>>, 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.
// the snippet below, with `-e` being used to execute only one line of
// AppleScript.
//
// ```
// osascript -e 'tell application "Finder" to delete { POSIX file "file1", POSIX "file2" }'
// ```
let mut command = Command::new("osascript");
let posix_files = full_paths
.iter()
.map(|p| {
let path_b = p.as_ref().as_os_str().as_encoded_bytes();
match std::str::from_utf8(path_b) {
.map(|path| {
let path_bytes = path.as_ref().as_os_str().as_encoded_bytes();

match std::str::from_utf8(path_bytes) {
Ok(path_utf8) => format!(r#"POSIX file "{}""#, esc_quote(path_utf8)), // utf-8 path, escape \"
Err(_) => format!(r#"POSIX file "{}""#, esc_quote(&percent_encode(path_b))), // binary path, %-encode it and escape \"
Err(_) => format!(r#"POSIX file "{}""#, esc_quote(&percent_encode(path_bytes))), // binary path, %-encode it and escape \"
}
})
.collect::<Vec<String>>()
.join(", ");
let script = format!("tell application \"Finder\" to delete {{ {posix_files} }}");

// When `with_info` is requested, we convert the Finder object references
// returned by `delete` into POSIX paths using `as alias`. The results are
// newline-delimited so we can split them reliably (commas would be
// ambiguous for filenames containing commas).
let script = if with_info {
format!(
r#"tell application "Finder"
set trashedItems to delete {{ {posix_files} }}
if class of trashedItems is not list then set trashedItems to {{trashedItems}}
set posixPaths to ""
repeat with t in trashedItems
if posixPaths is not "" then set posixPaths to posixPaths & linefeed
set posixPaths to posixPaths & POSIX path of (t as alias)
end repeat
return posixPaths
end tell"#
)
} else {
format!("tell application \"Finder\" to delete {{ {posix_files} }}")
};

let argv: Vec<OsString> = vec!["-e".into(), script.into()];
command.args(argv);
Expand All @@ -149,7 +204,37 @@ fn delete_using_finder<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error> {
}
};
}
Ok(())

if with_info {
let stdout = String::from_utf8_lossy(&result.stdout);
let time_deleted = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or(-1);

// In practice, the Finder `delete` command returns results in the same
// order as the input. We rely on this to pair trash paths with their
// original paths via `.zip()`.
let trash_items: Vec<TrashItem> = stdout
.lines()
.zip(full_paths.iter())
.map(|(trash_path, original_path)| {
let trash_path = Path::new(trash_path);
let original = original_path.as_ref();

TrashItem {
id: trash_path.as_os_str().to_os_string(),
name: original.file_name().map(|name| name.to_os_string()).unwrap_or_default(),
original_parent: original.parent().map(Path::to_owned).unwrap_or_default(),
time_deleted,
}
})
.collect();

Ok(Some(trash_items))
} else {
Ok(None)
}
}

/// std's from_utf8_lossy, but non-utf8 byte sequences are %-encoded instead of being replaced by a special symbol.
Expand Down
Loading
Loading