use std::{
ffi::OsString,
path::{Path, PathBuf},
process::Command,
};
use log::trace;
use objc2_foundation::{NSFileManager, NSString, NSURL};
use crate::{into_unknown, Error, TrashContext};
#[derive(Copy, Clone, Debug)]
pub enum DeleteMethod {
Finder,
NsFileManager,
}
impl DeleteMethod {
pub const fn new() -> Self {
DeleteMethod::Finder
}
}
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(crate) fn delete_all_canonicalized(&self, full_paths: Vec<PathBuf>) -> Result<(), Error> {
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<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error> {
trace!("Starting delete_using_file_mgr");
let file_mgr = NSFileManager::defaultManager();
for path in full_paths {
let path = path.as_ref().as_os_str().as_encoded_bytes();
let path = match std::str::from_utf8(path) {
Ok(path_utf8) => NSString::from_str(path_utf8), Err(_) => NSString::from_str(&percent_encode(path)), };
trace!("Starting fileURLWithPath");
let url = NSURL::fileURLWithPath(&path);
trace!("Finished fileURLWithPath");
trace!("Calling trashItemAtURL");
let res = file_mgr.trashItemAtURL_resultingItemURL_error(&url, None);
trace!("Finished trashItemAtURL");
if let Err(err) = res {
return Err(Error::Unknown {
description: format!("While deleting '{:?}', `trashItemAtURL` failed: {err}", &path),
});
}
}
Ok(())
}
fn delete_using_finder<P: AsRef<Path>>(full_paths: &[P]) -> Result<(), Error> {
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) {
Ok(path_utf8) => format!(r#"POSIX file "{}""#, esc_quote(path_utf8)), Err(_) => format!(r#"POSIX file "{}""#, esc_quote(&percent_encode(path_b))), }
})
.collect::<Vec<String>>()
.join(", ");
let script = format!("tell application \"Finder\" to delete {{ {posix_files} }}");
let argv: Vec<OsString> = vec!["-e".into(), script.into()];
command.args(argv);
let result = command.output().map_err(into_unknown)?;
if !result.status.success() {
let stderr = String::from_utf8_lossy(&result.stderr);
match result.status.code() {
None => {
return Err(Error::Unknown {
description: format!("The AppleScript exited with error. stderr: {}", stderr),
})
}
Some(code) => {
return Err(Error::Os {
code,
description: format!("The AppleScript exited with error. stderr: {}", stderr),
})
}
};
}
Ok(())
}
use std::borrow::Cow;
fn percent_encode(input: &[u8]) -> Cow<'_, str> {
use percent_encoding::percent_encode_byte as b2pc;
let mut iter = input.utf8_chunks().peekable();
if let Some(chunk) = iter.peek() {
if chunk.invalid().is_empty() {
return Cow::Borrowed(chunk.valid());
}
} else {
return Cow::Borrowed("");
};
let mut res = String::with_capacity(input.len());
for chunk in iter {
res.push_str(chunk.valid());
let invalid = chunk.invalid();
if !invalid.is_empty() {
for byte in invalid {
res.push_str(b2pc(*byte));
}
}
}
Cow::Owned(res)
}
fn esc_quote(s: &str) -> Cow<'_, str> {
if s.contains(['"', '\\']) {
let mut r = String::with_capacity(s.len());
let chars = s.chars();
for c in chars {
match c {
'"' | '\\' => {
r.push('\\');
r.push(c);
} _ => {
r.push(c);
} }
}
Cow::Owned(r)
} else {
Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests;