Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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